From a8015295877f55acde0b57769939c3ff037ea81d Mon Sep 17 00:00:00 2001 From: Ali Al Dallal Date: Tue, 18 Aug 2026 15:26:30 -0400 Subject: [PATCH 1/2] ci: changed-lines coverage report -- diff-cover over Go+Vitest lcov (goal 0109 phase 2a) Vitest emits lcov alongside text; test-go uploads its coverprofile; a new PR-only diff-coverage job converts it via gcov2lcov and runs diff-cover against the PR's merge base over both reports. INFORMATIONAL (continue-on-error, outside ci-gate): this repo's layering proves components in e2e, not unit tests, so an industry-default fail-under would fail honest UI PRs -- the floor gets set from a track record of real PR numbers, the same measure-then-promote shape as the bridge smoke job. Verified locally end to end (gcov2lcov 21k-line lcov; diff-cover runs clean). Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ frontend/vite.config.ts | 4 ++++ 2 files changed, 54 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e42d6cb..a184cc75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -318,6 +318,56 @@ jobs: # compile without it, even for tests that never touch the embedded # assets themselves. - run: go test . ./internal/... -race -coverprofile=cover.out && bash scripts/check-go-coverage.sh cover.out + # Consumed by diff-coverage below -- reuse the profile this job + # already produced instead of paying a second full -race run. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: go-coverprofile + path: cover.out + + # Changed-lines coverage (goal 0109 phase 2, the clean-as-you-code + # counterpart to the gocognit gate): diff-cover reports how much of + # THIS PR's changed lines the unit layers cover, from the Go + # coverprofile (via gcov2lcov) + Vitest's lcov. INFORMATIONAL for + # now -- continue-on-error and outside ci-gate's needs -- because + # this repo's layering deliberately proves components in e2e, not + # unit tests, so an industry-default fail-under would fail honest UI + # PRs; the floor gets set from a track record of real PR numbers, + # the same measure-then-promote shape as webview-bridge-smoke. + diff-coverage: + needs: [changes, frontend, test-go] + if: success() && github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + continue-on-error: true + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: go-coverprofile + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.25' + cache: true + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - run: npm ci + working-directory: frontend + - run: npm run test + working-directory: frontend + - run: go install github.com/jandelgado/gcov2lcov@v1.1.1 + - run: gcov2lcov -infile cover.out -outfile go-lcov.info + - run: pipx install diff-cover + - name: Changed-lines coverage report + run: | + diff-cover go-lcov.info frontend/coverage/lcov.info \ + --compare-branch "origin/${{ github.base_ref }}" \ + --show-uncovered e2e: needs: [changes, frontend] diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8bed7c33..adef0cc3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -90,6 +90,10 @@ export default defineConfig({ exclude: [...configDefaults.exclude, "e2e/**"], coverage: { provider: "v8", + // lcov alongside the default text output: the changed-lines + // coverage gate (goal 0109, diff-cover in CI) consumes + // coverage/lcov.info; text stays for humans. + reporter: ["text", "lcov"], // Hand-written source only -- generated Wails bindings are // exempt for the same we-don't-own-their-shape reason // scripts/check-loc.sh exempts them. From 3a7e6f758c7e754a9f12a71009b6e872946ccdc4 Mon Sep 17 00:00:00 2001 From: Ali Al Dallal Date: Tue, 18 Aug 2026 15:36:11 -0400 Subject: [PATCH 2/2] test: explicit returns after nil-guard t.Fatal -- SA5011 under CI's fresh-cache lint The fetch-depth:0 change invalidated the lint action's cache and a fresh staticcheck pass lost t.Fatal's noreturn fact under the CI build config (CGO_ENABLED=0/Go 1.25), flagging three latent guard-then-deref sites in test files that every cached run had passed. Explicit returns make the control flow analyzer-independent. Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-Authored-By: Claude Fable 5 --- internal/services/mcpsvc/millmcpservice_atlas_test.go | 2 ++ internal/webviewbridgesmoke/checks_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/internal/services/mcpsvc/millmcpservice_atlas_test.go b/internal/services/mcpsvc/millmcpservice_atlas_test.go index a563a135..cf8f65c2 100644 --- a/internal/services/mcpsvc/millmcpservice_atlas_test.go +++ b/internal/services/mcpsvc/millmcpservice_atlas_test.go @@ -124,6 +124,7 @@ func TestAtlasMCP_ListKinds_IncludesSeededTopicWithDeclaredFields(t *testing.T) } if topic == nil { t.Fatalf("seeded Topic kind not found in atlas_list_kinds: %+v", out.Kinds) + return // staticcheck SA5011: t.Fatalf's noreturn fact is lost under CI's build config } fieldKeys := map[string]bool{} for _, f := range topic.Fields { @@ -186,6 +187,7 @@ func TestAtlasMCP_SearchCards_MatchesTitleAndReportsParent(t *testing.T) { } if titleMatch == nil { t.Fatalf("atlas_search_cards(Ada) = %+v, want a match on Ada Lovelace's own title", out.Matches) + return // staticcheck SA5011: t.Fatalf's noreturn fact is lost under CI's build config } if titleMatch.ParentID != exampleArea.ID { t.Errorf("match.ParentID = %q, want %q", titleMatch.ParentID, exampleArea.ID) diff --git a/internal/webviewbridgesmoke/checks_test.go b/internal/webviewbridgesmoke/checks_test.go index bc348af5..c7606f8e 100644 --- a/internal/webviewbridgesmoke/checks_test.go +++ b/internal/webviewbridgesmoke/checks_test.go @@ -362,6 +362,7 @@ func TestCheckStickyBorderColorFlip(t *testing.T) { } if createCall == nil { t.Fatal("CreateNote was never called") + return // staticcheck SA5011: t.Fatal's noreturn fact is lost under CI's build config } args, _ := createCall.args["args"].([]any) if len(args) != 3 || args[2] != "space-1" {