Skip to content

test: add coverage reporting with a threshold gate#117

Merged
rubenhensen merged 4 commits into
mainfrom
ci/test-coverage
Jul 2, 2026
Merged

test: add coverage reporting with a threshold gate#117
rubenhensen merged 4 commits into
mainfrom
ci/test-coverage

Conversation

@rubenhensen

@rubenhensen rubenhensen commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Closes #107.

Adds test-coverage measurement + a regression gate. Coverage wasn't measured before, so nothing stopped it eroding silently.

Changes

  • @vitest/coverage-v8 added; coverage configured in vite.config.ts (provider v8, text-summary + text + lcov reporters).
  • Scope: src/lib/** (the unit-tested server/lib code; routes are covered by e2e). One-off DB migration scripts (src/lib/server/migrations/**) are excluded so an untested future migration can't trip the gate on an unrelated PR.
  • CI test job now runs vitest run --coverage, so the thresholds are enforced on every PR.
  • coverage/ added to .gitignore and .prettierignore.

Threshold

Measured baseline over src/lib/** (migrations excluded) is ~20% statements / 30% branch / 9% functions / 19% lines (91 unit tests). Thresholds are set a couple of points below that:

metric threshold
statements 18
branches 27
functions 7
lines 17

This locks in a no-regression floor without failing on the current state (latest run: 25.7% stmts / 32.5% branch / 10.1% funcs / 25.1% lines — all above floor). The intent is to ratchet these up as coverage improves — they're deliberately conservative for a first gate.

@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

On it, @rubenhensen! I'm reviewing PR #117 now — checking the test coverage additions and CI regression gate for correctness and rule-compliance, and I'll post a consolidated review shortly.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the coverage + regression-gate change. Rule checks: all pass (conventional-commit title test:, Closes #107, no README/CSP/route/UI/security-note conflicts, dep is actually used, repo npm-audit-override policy untouched). Tests: 83/83 pass and the gate is green (33.55% stmt / 47.5% branch / 16.47% func / 33.57% line, all above the 30/40/15/30 floors).

No blocking issues — the gate is deliberately conservative and correctly locks in a no-regression floor. Two non-blocking nits inline about coverage scope and the tightest threshold margin.

Comment thread vite.config.ts
provider: 'v8',
reporter: ['text-summary', 'text', 'lcov'],
reportsDirectory: './coverage',
include: ['src/lib/**/*.{ts,js}'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: include: ['src/lib/**/*.{ts,js}'] also pulls in one-off DB migration files (src/lib/server/migrations/, e.g. legacy-api-keys.ts currently ~98%). Migrations aren't normally unit-tested, so a future untested migration would drag the overall percentages down and could trip this regression gate on an unrelated PR. Consider excluding 'src/lib/server/migrations/**' so the gate reflects testable lib code.

Comment thread vite.config.ts Outdated
thresholds: {
statements: 30,
branches: 40,
functions: 15,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the functions floor (15) sits only ~1.5pt below the 16.47% baseline (14/85 covered functions) — dropping a single covered function to 12/85 = 14.12% would trip the gate. Statements/lines (~3.5pt) and branches (~7.5pt) have healthier margins. Deliberate per the PR; just flagging it as the tightest margin to keep in mind when ratcheting thresholds up.

@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed! All rule checks pass, 83/83 tests are green, and the coverage gate is above its floors. Left two non-blocking nits in the review: migration files inflate the coverage scope a little, and the functions floor is your tightest margin worth keeping an eye on. Nothing blocking a merge.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

Thanks @dobby-coder — addressed in 366e435:

  • Migrations in coverage scope → added src/lib/server/migrations/** to exclude. This meaningfully lowered the baseline (that one well-tested migration file was carrying a lot of the number), so the gate now honestly reflects testable lib code: ~20% stmts / 30% branch / 9% funcs / 19% lines.
  • Threshold margins → retuned to 18 / 27 / 7 / 17 (a couple points below the new baseline), which also gives the functions floor a healthier ~2pt margin instead of the previous ~1.5pt. Comment updated to match. Ratchet-up intent unchanged.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

/dobby fix conflicts

@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

On it! I'll resolve the merge conflicts on this PR and get it cleaned up and ready for review.

Regenerated package-lock.json from the merged package.json so both the
main-side deps (pino, LICENSE) and this branch's @vitest/coverage-v8 are
reconciled. 91 unit tests pass; coverage gate passes above thresholds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

✅ Merge conflict resolved and pushed (9528375).

What happened: main moved ahead (LICENSE #116, structured logging #115, container scan/sign #118) which conflicted with this branch. I merged origin/main in and regenerated package-lock.json from the merged package.json — the only real conflict was the lockfile, where both branches added alphabetically-adjacent entries (ast-v8-to-istanbul vs atomic-sleep). Both dependency families (pino from main, @vitest/coverage-v8 from this branch) are now reconciled. All 91 unit tests pass and the coverage gate is green (25.7% stmts / 32.5% branch / 10.1% funcs / 25.1% lines, above the 18/27/7/17 floors).

⚠️ One manual step needed — .github/workflows/ci.yml. The merge should also bring in main's cosign-signing + image-scan additions (from #118) alongside this branch's --coverage test step. I'm not permitted to push changes under .github/workflows/ (the App lacks the workflows scope), so I left ci.yml at this branch's version. @rubenhensen, please apply the patch below so the workflow keeps main's signing/scan jobs:

ci.yml patch — git apply on the ci/test-coverage branch
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69169b3..844e86f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -172,6 +172,7 @@ jobs:
     permissions:
       contents: read
       packages: write
+      id-token: write # keyless cosign signing via OIDC
     steps:
       - name: Download digests
         uses: actions/download-artifact@v8
@@ -202,3 +203,61 @@ jobs:
           docker buildx imagetools create \
             $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
             $(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
+      # Sign only on pushes to main/releases — not PRs. Signing every PR would
+      # record throwaway pr-N images in the public Rekor transparency log and
+      # leave orphan .sig tags in GHCR. The cosign path was smoke-tested on the
+      # PR that introduced it (#118).
+      - name: Install cosign
+        if: github.event_name != 'pull_request'
+        uses: sigstore/cosign-installer@v3
+      - name: Sign the published image (keyless)
+        if: github.event_name != 'pull_request'
+        run: |
+          tag=$(jq -cr '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
+          digest=$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}')
+          cosign sign --yes "ghcr.io/${{ github.repository }}@${digest}"
+
+  # Build the runtime image and scan it for OS/dependency vulnerabilities.
+  # Non-blocking for now (exit-code 0): findings surface in the Security tab.
+  # Flip exit-code to 1 to gate once the baseline is clean.
+  image-scan:
+    name: Image Scan
+    needs: build
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      security-events: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v6
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v4
+      - name: Build image (amd64) for scanning
+        uses: docker/build-push-action@v7
+        with:
+          context: .
+          file: docker/Dockerfile
+          platforms: linux/amd64
+          load: true
+          tags: postguard-business:scan
+          cache-from: type=gha
+      # Run Trivy from its official image rather than the GitHub Action, which
+      # had a supply-chain compromise advisory (GHSA-69fq-xp46-6x23).
+      - name: Trivy vulnerability scan
+        run: |
+          docker run --rm \
+            -v /var/run/docker.sock:/var/run/docker.sock \
+            -v "$PWD:/work" \
+            aquasec/trivy:0.72.0 image \
+            --severity HIGH,CRITICAL \
+            --ignore-unfixed \
+            --format sarif \
+            --output /work/trivy-results.sarif \
+            --exit-code 0 \
+            postguard-business:scan
+      - name: Upload Trivy results
+        if: hashFiles('trivy-results.sarif') != ''
+        uses: github/codeql-action/upload-sarif@v3
+        with:
+          sarif_file: trivy-results.sarif
+          category: trivy

Save the diff block to a file and run git apply <file> (or git checkout origin/main -- .github/workflows/ci.yml then re-add the --coverage flag on the test step). Everything else is merged and pushed.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules Dobby 2 — consolidated review (REQUEST_CHANGES)

The coverage gate itself works (gate passes exit 0, 91/91 tests green), and the mechanism is sound. But the conflict-resolution merge that brought this branch up to date (9528375) took the branch-side .github/workflows/ci.yml wholesale and silently dropped everything PR #118 ("scan and sign published container images") added to main. This PR is scoped to coverage only, so that deletion is an unintended security regression that must be restored before merge.

Evidence: on main (d1d23b1) ci.yml is 263 lines with 5 cosign references and an image-scan (Trivy) job; on this branch it is 204 lines with none. Merging #117 as-is would revert #118, which is already live on main.

Both the rule pass (Haiku sub-agents over ~/dobby-memory/rules) and Review Dobby 2 converge on the same three findings below — no additional distinct issues surfaced. conflict-resolution-check-noop, the repo security notes, and promised-vs-delivered all flagged non-compliance; PR-title, tests-required, and justification-paragraph rules all passed.

Findings

  1. (blocker) ci.yml — the merge removed id-token: write from the finalize job, killing keyless cosign OIDC signing.
  2. (blocker) ci.yml — the merge deleted the Install cosign + keyless-sign steps AND the entire image-scan (Trivy) job from #118.
  3. (nit) vite.config.ts / PR description — the description's threshold table (30/30/40/15, baseline ~33.5/47.5/16.5/33.6) is stale; committed thresholds are 18/27/7/17 (retuned in 366e435 after excluding migrations).

Suggested fix: re-merge origin/main into the branch keeping BOTH the coverage change (the --coverage flag + vite.config.ts block) and #118's signing/scanning, then refresh the PR description's threshold table.

Caveat for the fixer: the fix touches .github/workflows/ci.yml. Per workflows-permission-recurring-blocker, dobby-coder[bot] may lack workflows:write — though dobby-yivi-agent[bot] already pushed the merge that modified ci.yml on this branch, so a push should be feasible here. If the push is rejected, deliver the ci.yml portion as a patch-in-comment. Also: this PR is currently ready-for-review with open blockers — consider flipping back to draft until the regression is resolved.

Comment thread .github/workflows/ci.yml
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — security regression. The conflict-resolution merge (9528375) took the branch-side ci.yml wholesale and dropped id-token: write from the finalize job's permissions — the OIDC token required for keyless cosign signing added in #118. Net effect: merging #117 silently reverts #118 (already on main). Restore this permission when re-merging main.

Comment thread .github/workflows/ci.yml
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — security regression. Everything after this line was deleted by the same bad merge: the Install cosign + Sign the published image (keyless) steps AND the entire image-scan (Trivy) job that #118 added to main. On main (d1d23b1) ci.yml is 263 lines / 5 cosign refs / one image-scan job; on this branch it is 204 lines with none. This PR is scoped to coverage only, so this is an unintended regression — re-merge main keeping both the coverage change and #118's signing/scanning.

Comment thread vite.config.ts
// 9% funcs / 19% lines. Thresholds sit a couple points below to lock in a
// no-regression floor without tripping on the current state; ratchet up
// as lib coverage improves.
thresholds: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. The PR description's threshold table says statements 30 / lines 30 / branches 40 / functions 15 (baseline ~33.5/47.5/16.5/33.6), but the committed thresholds here are 18 / 27 / 7 / 17 (retuned in 366e435 after excluding migrations). Update the description so reviewers see the actual gate.

@dobby-coder dobby-coder Bot marked this pull request as draft July 1, 2026 14:32
@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ CI regression fix ready — needs manual apply (workflows permission blocker)

The Rule Checker was right: my earlier conflict-resolution merge (9528375) took the branch-side .github/workflows/ci.yml wholesale and silently dropped everything PR #118 (container image scan + keyless cosign signing) added to main. This PR is scoped to coverage only, so merging it as-is would revert #118 — a security regression.

I re-took main's ci.yml and re-applied only this PR's change (the --coverage flag on the vitest step). Verified locally against origin/main, the workflow now differs by exactly that one line — nothing else. I committed it locally (9784240) but dobby-coder[bot] lacks the workflows: write scope, so the push is rejected (refusing to allow a GitHub App to create or update workflow ... without workflows permission). So this must be applied by a human (or by dobby-yivi-agent[bot], which does have the scope — it pushed the earlier merge on this branch).

✅ Verification (locally, on the fixed branch)

check result
ci.yml vs origin/main differs by 1 line only (npx vitest runnpx vitest run --coverage) — #118's id-token: write, cosign steps, and image-scan/Trivy job all restored
npx vitest run --coverage 91/91 tests pass, gate exit 0
coverage 25.7% stmts / 32.5% branch / 10.1% funcs / 25.1% lines — all above the 18/27/7/17 floors

Simplest way to apply (recommended)

git fetch origin
git checkout ci/test-coverage
git checkout origin/main -- .github/workflows/ci.yml   # restore #118 in full
sed -i 's/npx vitest run$/npx vitest run --coverage/' .github/workflows/ci.yml
git commit -am "ci: restore #118 cosign signing + Trivy scan dropped by conflict merge"
git push

Or apply this patch on top of the current branch

ci.yml patch — save to a file and git apply
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69169b3..844e86f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -172,6 +172,7 @@ jobs:
     permissions:
       contents: read
       packages: write
+      id-token: write # keyless cosign signing via OIDC
     steps:
       - name: Download digests
         uses: actions/download-artifact@v8
@@ -202,3 +203,61 @@ jobs:
           docker buildx imagetools create \
             $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
             $(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
+      # Sign only on pushes to main/releases — not PRs. Signing every PR would
+      # record throwaway pr-N images in the public Rekor transparency log and
+      # leave orphan .sig tags in GHCR. The cosign path was smoke-tested on the
+      # PR that introduced it (#118).
+      - name: Install cosign
+        if: github.event_name != 'pull_request'
+        uses: sigstore/cosign-installer@v3
+      - name: Sign the published image (keyless)
+        if: github.event_name != 'pull_request'
+        run: |
+          tag=$(jq -cr '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
+          digest=$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}')
+          cosign sign --yes "ghcr.io/${{ github.repository }}@${digest}"
+
+  # Build the runtime image and scan it for OS/dependency vulnerabilities.
+  # Non-blocking for now (exit-code 0): findings surface in the Security tab.
+  # Flip exit-code to 1 to gate once the baseline is clean.
+  image-scan:
+    name: Image Scan
+    needs: build
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      security-events: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v6
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v4
+      - name: Build image (amd64) for scanning
+        uses: docker/build-push-action@v7
+        with:
+          context: .
+          file: docker/Dockerfile
+          platforms: linux/amd64
+          load: true
+          tags: postguard-business:scan
+          cache-from: type=gha
+      # Run Trivy from its official image rather than the GitHub Action, which
+      # had a supply-chain compromise advisory (GHSA-69fq-xp46-6x23).
+      - name: Trivy vulnerability scan
+        run: |
+          docker run --rm \
+            -v /var/run/docker.sock:/var/run/docker.sock \
+            -v "$PWD:/work" \
+            aquasec/trivy:0.72.0 image \
+            --severity HIGH,CRITICAL \
+            --ignore-unfixed \
+            --format sarif \
+            --output /work/trivy-results.sarif \
+            --exit-code 0 \
+            postguard-business:scan
+      - name: Upload Trivy results
+        if: hashFiles('trivy-results.sarif') != ''
+        uses: github/codeql-action/upload-sarif@v3
+        with:
+          sarif_file: trivy-results.sarif
+          category: trivy

Also refreshed the PR description's threshold table (was stale at 30/30/40/15 baseline ~33.5%; committed values are 18/27/7/17, post-migration-exclusion baseline ~20/30/9/19) and flipped the PR back to draft while this regression is outstanding.

@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Hey @rubenhensen — found and fixed the regression, but hit a wall on the push.

The conflict-resolution merge (#9528375) silently reverted PR #118 (container image scan + keyless cosign signing) that's live on main — it took the branch-side ci.yml wholesale. I've re-applied only this PR's actual change (the --coverage flag on the vitest step) on top of main's ci.yml, so the two are now identical except for that one line. 91/91 tests pass locally.

Blocked: dobby-coder[bot] doesn't have workflows:write scope, so GitHub rejected the push. I've left the fix as a copy-pastable patch in a comment with a simpler git checkout origin/main -- .github/workflows/ci.yml recipe you can apply directly. Could you (or dobby-yivi-agent[bot]) apply it?

Also refreshed the threshold table in the PR description (was stale at 30/30/40/15; now 18/27/7/17 post-migration-exclusion) and flipped the PR back to draft while this regression is outstanding.

@rubenhensen rubenhensen marked this pull request as ready for review July 1, 2026 14:46
@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

On it! I'm picking up PR #117 and starting a full review for correctness and rule-compliance — I'll post a consolidated review with a clear action once I'm done.

@rubenhensen

Copy link
Copy Markdown
Contributor Author

Applied the ci.yml fix (I have workflows:write, so the push went through) in 4c0bc48:

  • Restored ci: scan and sign published container images #118 in full — took origin/main's ci.yml and re-applied only the --coverage flag on the vitest step. git diff origin/main -- .github/workflows/ci.yml is now exactly that one line, so the cosign signing, id-token: write, and image-scan/Trivy job are all back (no ci: scan and sign published container images #118 revert).
  • Kept the lockfile minimal — the earlier conflict merge had regenerated package-lock.json and incidentally bumped prettier 3.8.3 → 3.9.4, which reformats src/lib/global.scss and was failing lint. Reset package.json/package-lock.json to main and re-added just @vitest/coverage-v8, so the only lock delta vs main is the coverage provider. prettier is back to 3.8.3 and lint is clean.

Coverage gate green locally (25.7 / 32.5 / 10.1 / 25.1, above the 18/27/7/17 floors). Marked ready for review.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧦 Dobby reviewed this PR (code review + per-rule compliance check).

The coverage gate itself is solid: scope is sensibly limited to src/lib/** with migrations excluded, thresholds sit a couple points below the measured baseline as a no-regression floor, CI enforces it via --coverage, and coverage/ is git/prettier-ignored. Tests pass (91/91), all above the floors. No rule violations in the coverage config, CI, or ignore changes.

One issue found:

  • Unused zod runtime dependency (see inline comment on package.json). zod@^4.4.3 is added to dependencies but imported nowhere in source. It's unrelated to this PR's purpose and should be removed or split out.

Requesting changes only for the stray zod dependency — the coverage work is good to go once that's cleaned up.

Comment thread package.json
"sass": "^1.100.0",
"svelte-i18n": "^4.0.1"
"svelte-i18n": "^4.0.1",
"zod": "^4.4.3"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zod@^4.4.3 is added as a runtime dependency here, but zod is imported nowhere in the source tree (grep -rn zod src/ finds only Dutch words in locale files). This looks like a stray addition — it's unrelated to the coverage gate and contradicts the "keep lock minimal" intent of the cleanup commit (4c0bc48). Per our check-dep-is-used-before-bumping rule, a dependency should be verified as used before it's added. Please drop zod (and its lockfile entry) from this PR, or split it into its own PR alongside the code that actually uses it.

@dobby-coder

dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed! The coverage gate looks solid — CI config, ignore changes, and all 91 tests check out. I left one REQUEST_CHANGES comment on package.json: zod@^4.4.3 was added to runtime dependencies but isn't imported anywhere in src (the grep hits are just Dutch locale words). Could you remove it or split it into a separate PR? Everything else is good to go.

@rubenhensen rubenhensen merged commit ec232da into main Jul 2, 2026
16 checks passed
@rubenhensen rubenhensen deleted the ci/test-coverage branch July 2, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: add test-coverage reporting and a threshold gate

1 participant