From 54d5ac93c482c3f1d8f5444e0d463196aa3e1201 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 19:00:01 +0900 Subject: [PATCH 01/30] test: require complete frontend production coverage scope --- src/lib/frontendCoverageScope.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/lib/frontendCoverageScope.test.ts diff --git a/src/lib/frontendCoverageScope.test.ts b/src/lib/frontendCoverageScope.test.ts new file mode 100644 index 000000000..127ac6604 --- /dev/null +++ b/src/lib/frontendCoverageScope.test.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const config = readFileSync(new URL("../../vitest.config.ts", import.meta.url), "utf8"); + +describe("frontend production coverage scope", () => { + it("measures every source-controlled production TypeScript module", () => { + expect(config).toContain('include: ["src/lib/**/*.ts", "src/routes/**/*.ts"]'); + expect(config).toContain('exclude: ["**/*.test.ts", "**/*.d.ts"]'); + for (const legacyAllowlistEntry of [ + "src/lib/api.ts", + "src/lib/treemap.ts", + "src/lib/fmt.ts", + "src/lib/dupeGuard.ts", + "src/lib/verdictBadge.ts", + ]) { + expect(config).not.toContain(` "${legacyAllowlistEntry}",`); + } + }); +}); From db0968b7956e9ba17e2c7fe7e8ded6eba7eadee1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 19:11:37 +0900 Subject: [PATCH 02/30] fix(coverage): measure all production TypeScript --- vitest.config.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 198e3dcb8..a7566285d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,19 +5,9 @@ export default defineConfig({ include: ["src/**/*.test.ts"], coverage: { provider: "v8", - // ponytail: 커버리지는 헤드리스로 검증 가능한 순수 로직과 mockable Tauri API 래퍼만 측정. - // Svelte 컴포넌트는 GUI·통합 검증 영역 (cargo test + 수동 체크리스트) - include: [ - "src/lib/api.ts", - "src/lib/treemap.ts", - "src/lib/fmt.ts", - "src/lib/dupeGuard.ts", - "src/lib/verdictBadge.ts", - ], + include: ["src/lib/**/*.ts", "src/routes/**/*.ts"], + exclude: ["**/*.test.ts", "**/*.d.ts"], reporter: ["text", "json", "json-summary"], - // ponytail: 위 include 5개 순수 로직 파일은 헤드리스로 완전 검증 가능하므로 - // 네 지표 모두 100%로 고정한다. 이 게이트는 scope를 넓히지 않는다 — - // Svelte 컴포넌트는 여전히 cargo test + 수동 체크리스트로 검증한다. thresholds: { statements: 100, branches: 100, From a50b0a2f49eb5187fedfd037f509525ee2e7dce6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 19:13:06 +0900 Subject: [PATCH 03/30] test(coverage): require exact-head fail-closed evidence --- src/lib/coverageEvidenceWorkflow.test.ts | 128 +++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/lib/coverageEvidenceWorkflow.test.ts diff --git a/src/lib/coverageEvidenceWorkflow.test.ts b/src/lib/coverageEvidenceWorkflow.test.ts new file mode 100644 index 000000000..8f03d38ad --- /dev/null +++ b/src/lib/coverageEvidenceWorkflow.test.ts @@ -0,0 +1,128 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync( + new URL('../../.github/workflows/test.yml', import.meta.url), + 'utf8', +); +const diagnosticHelper = readFileSync( + new URL('../../.github/scripts/bound-coverage-command-diagnostic.sh', import.meta.url), + 'utf8', +); + +describe('Test workflow coverage evidence contract', () => { + it('binds coverage evidence to the exact pull-request head', () => { + expect(workflow).toContain( + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ); + expect(workflow).toContain( + 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}', + ); + }); + + it('binds every checkout-bearing Test job to the exact current head', () => { + const exactHeadCheckout = + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; + expect(workflow.split(exactHeadCheckout).length - 1).toBe(4); + }); + + it('requires every Rust test and coverage invocation to honor the committed lockfile', () => { + const rustExecutionLines = workflow + .split('\n') + .map((line) => line.trim()) + .filter( + (line) => + line.startsWith('cargo test ') || line.startsWith('cargo llvm-cov '), + ); + + expect(rustExecutionLines.length).toBeGreaterThan(0); + for (const line of rustExecutionLines) { + expect(line, `unlocked Rust CI command: ${line}`).toContain('--locked'); + } + }); + + it('measures Rust branch coverage instead of synthesizing percentages', () => { + expect(workflow).toContain('tool: cargo-llvm-cov'); + expect(workflow).toContain( + 'cargo llvm-cov --locked --no-cfg-coverage --no-cfg-coverage-nightly --all-features --manifest-path src-tauri/Cargo.toml --branch --json --output-path coverage.json', + ); + expect(workflow).not.toContain('--summary-only'); + expect(workflow).toContain('coverage.json'); + expect(workflow).toContain('coverage-evidence.json'); + }); + + it('keeps coverage instrumentation from changing production cfg semantics', () => { + expect(workflow).toContain('--no-cfg-coverage'); + expect(workflow).toContain('--no-cfg-coverage-nightly'); + }); + + it('preserves bounded exact-head metric diagnostics when the 100% gate fails', () => { + expect(workflow).toContain('coverage-diagnostic.json'); + expect(workflow).toContain('name: coverage-diagnostic-${{ env.HEAD_SHA }}'); + expect(workflow).toContain('path: coverage-diagnostic.json'); + expect(workflow).toContain('if: always()'); + expect(workflow).toContain('regions: totals?.regions ?? null'); + expect(workflow).toContain('branches: totals?.branches ?? null'); + expect(workflow).toContain('functions: totals?.functions ?? null'); + expect(workflow).toContain('lines: totals?.lines ?? null'); + }); + + it('preserves both ends of a bounded sanitized command diagnostic when measurement itself fails', () => { + expect(workflow).toContain('id: rust-coverage'); + expect(workflow).toContain('coverage-command.raw.log'); + expect(workflow).toContain('coverage-command.bounded.log'); + expect(workflow).toContain('coverage-command-diagnostic.log'); + expect(diagnosticHelper).toContain('max_total_bytes=32768'); + expect(diagnosticHelper).toContain('edge_bytes=9000'); + expect(diagnosticHelper).toContain('head -c "$edge_bytes" "$line_bounded_log"'); + expect(diagnosticHelper).toContain('tail -c "$edge_bytes" "$line_bounded_log"'); + expect(workflow).toContain("replaceAll(workspace, '')"); + expect(workflow).toContain("replaceAll(home, '')"); + expect(workflow).toContain( + "if: failure() && steps.rust-coverage.outcome == 'failure'", + ); + }); + + it('preserves the authoritative coverage exit status when diagnostic rendering fails', () => { + const measureStart = workflow.indexOf('name: Measure exact-head Rust coverage'); + const uploadStart = workflow.indexOf( + 'name: Upload bounded coverage command diagnostic', + measureStart, + ); + expect(measureStart).toBeGreaterThanOrEqual(0); + expect(uploadStart).toBeGreaterThan(measureStart); + const measureStep = workflow.slice(measureStart, uploadStart); + expect(measureStep).toContain('coverage_status=$?'); + expect(measureStep).toContain('diagnostic_status=$?'); + expect(measureStep).toContain('redaction_status=$?'); + expect(measureStep).toContain('coverage diagnostic rendering failed'); + expect(measureStep).toContain('exit "$coverage_status"'); + }); + + it('identifies the largest exact-head Rust coverage gaps without leaking runner paths', () => { + expect(workflow).toContain('top_uncovered_files'); + expect(workflow).toContain("const marker = '/src-tauri/'"); + expect(workflow).toContain('uncovered_regions'); + expect(workflow).toContain('uncovered_branches'); + expect(workflow).toContain('uncovered_functions'); + expect(workflow).toContain('uncovered_lines'); + }); + + it('preserves bounded frontend diagnostics when the all-production threshold fails', () => { + expect(workflow).toContain('name: Build bounded frontend coverage diagnostic'); + expect(workflow).toContain("readFileSync('coverage/coverage-final.json', 'utf8')"); + expect(workflow).toContain("readFileSync('coverage/coverage-summary.json', 'utf8')"); + expect(workflow).toContain('frontend_top_uncovered_files'); + expect(workflow).toContain('frontend-coverage-diagnostic.json'); + }); + + it('uploads fail-closed evidence under the organization contract name', () => { + expect(workflow).toContain('name: coverage-evidence'); + expect(workflow).toContain('path: coverage-evidence.json'); + expect(workflow).toContain('if-no-files-found: error'); + expect(workflow).toContain('statement_coverage'); + expect(workflow).toContain('branch_coverage'); + expect(workflow).toContain('function_coverage'); + expect(workflow).toContain('line_coverage'); + }); +}); From 05ce06bdb56debe504c7f95f36e9d668fc01e8e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 19:13:33 +0900 Subject: [PATCH 04/30] test(coverage): add bounded command diagnostic helper --- .../bound-coverage-command-diagnostic.sh | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/scripts/bound-coverage-command-diagnostic.sh diff --git a/.github/scripts/bound-coverage-command-diagnostic.sh b/.github/scripts/bound-coverage-command-diagnostic.sh new file mode 100644 index 000000000..52a51984e --- /dev/null +++ b/.github/scripts/bound-coverage-command-diagnostic.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + printf 'usage: %s \n' "$0" >&2 + exit 64 +fi + +raw_log="$1" +bounded_log="$2" +max_total_bytes=32768 +edge_bytes=9000 +error_focus_bytes=8000 +other_focus_bytes=4000 +max_line_bytes=2048 +line_bounded_log="${bounded_log}.line-bounded.$$" +error_focus_log="${bounded_log}.error-focus.$$" +other_focus_log="${bounded_log}.other-focus.$$" + +cleanup() { + rm -f "$line_bounded_log" "$error_focus_log" "$other_focus_log" +} +trap cleanup EXIT + +LC_ALL=C awk -v max_bytes="$max_line_bytes" '{ + if (length($0) > max_bytes) { + print substr($0, 1, max_bytes) " ... [line truncated]" + } else { + print + } +}' "$raw_log" > "$line_bounded_log" + +diagnostic_bytes="$(wc -c < "$line_bounded_log" | tr -d ' ')" +if (( diagnostic_bytes <= max_total_bytes )); then + cp "$line_bounded_log" "$bounded_log" +else + LC_ALL=C awk ' + { + plain = $0 + gsub(/\033\[[0-9;]*m/, "", plain) + } + plain ~ /^thread .* panicked at / { + print + panic_context = 6 + in_error = 0 + next + } + panic_context > 0 { + print + panic_context-- + next + } + plain ~ /^(error(\[[^]]+\])?:|fatal:|Caused by:)/ { + print + in_error = 1 + next + } + in_error && plain ~ /^[[:space:]]*(--> |[0-9]+[[:space:]]*\||\|[[:space:]]|= (note|help):|(note|help):)/ { + print + next + } + { in_error = 0 } + ' "$line_bounded_log" > "$error_focus_log" + + LC_ALL=C awk ' + { + plain = $0 + gsub(/\033\[[0-9;]*m/, "", plain) + } + plain ~ /^warning(\[[^]]+\])?:|^[[:space:]]*= (note|help):|^[[:space:]]*(note|help):/ { print } + ' "$line_bounded_log" > "$other_focus_log" + + head -c "$edge_bytes" "$line_bounded_log" > "$bounded_log" + if [[ -s "$error_focus_log" ]]; then + printf '\n--- focused compiler errors and test panics ---\n' >> "$bounded_log" + head -c "$error_focus_bytes" "$error_focus_log" >> "$bounded_log" + fi + if [[ -s "$other_focus_log" ]]; then + printf '\n--- focused compiler warnings and notes ---\n' >> "$bounded_log" + head -c "$other_focus_bytes" "$other_focus_log" >> "$bounded_log" + fi + printf '\n--- bounded diagnostic tail ---\n' >> "$bounded_log" + tail -c "$edge_bytes" "$line_bounded_log" >> "$bounded_log" +fi From 8b69c6da560a0af95e1756ae35d389c692667560 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 19:14:58 +0900 Subject: [PATCH 05/30] fix(coverage): restore exact-head fail-closed evidence pipeline --- .github/workflows/test.yml | 200 ++++++++++++++++++++++++++++++++++--- 1 file changed, 188 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b35c94808..ccb3efd07 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,6 @@ on: paths-ignore: - "docs/**" - "*.md" - # Content-checked by contract tests (vitest + cargo test) — must still run CI. - "!docs/doctoring/release-artifact-provenance.md" - "!docs/doctoring/tauri-content-security-policy.md" - "!docs/doctoring/model-artifact-integrity.md" @@ -18,7 +17,6 @@ on: paths-ignore: - "docs/**" - "*.md" - # Content-checked by contract tests (vitest + cargo test) — must still run CI. - "!docs/doctoring/release-artifact-provenance.md" - "!docs/doctoring/tauri-content-security-policy.md" - "!docs/doctoring/model-artifact-integrity.md" @@ -33,38 +31,103 @@ permissions: jobs: test: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + RUST_TEST_THREADS: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install Tauri system deps run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev lsof - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: workspaces: src-tauri cache-targets: false - name: Rust tests (includes unix symlink test) - run: cargo test --manifest-path src-tauri/Cargo.toml + run: cargo test --locked --manifest-path src-tauri/Cargo.toml - name: Headless cloud planner tests - run: cargo test --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-cloud-plan + run: cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-cloud-plan - name: Exact duplicate audit tests run: | - cargo test --manifest-path src-tauri/Cargo.toml --features cloud-cli duplicate_audit - cargo test --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-duplicate-audit + cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli duplicate_audit + cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-duplicate-audit - name: Extraction-free archive tree proof tests run: | - cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli archive_git_tree - cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli --bin disksage-archive-tree - cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli --test archive_tree_help_exit + cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli archive_git_tree + cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli --bin disksage-archive-tree + cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli --test archive_tree_help_exit - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20.19.0 - run: npm ci - run: npm test + - name: Frontend production coverage + id: frontend-coverage + run: npm run coverage + - name: Build bounded frontend coverage diagnostic + if: failure() && steps.frontend-coverage.outcome == 'failure' + run: | + node --input-type=module <<'NODE' + import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; + const sha = process.env.HEAD_SHA ?? ''; + const repository = process.env.GITHUB_REPOSITORY ?? ''; + if (!/^[0-9a-f]{40}$/.test(sha) || repository.length === 0) { + throw new Error('frontend coverage diagnostic identity is invalid'); + } + const finalCoverage = JSON.parse(readFileSync('coverage/coverage-final.json', 'utf8')); + const summaryCoverage = JSON.parse(readFileSync('coverage/coverage-summary.json', 'utf8')); + const frontendMarker = '/src/'; + const frontendRepositoryPath = (filename) => { + const normalized = String(filename ?? '').replaceAll('\\', '/'); + if (normalized.startsWith('src/')) return normalized; + const markerIndex = normalized.lastIndexOf(frontendMarker); + return markerIndex < 0 ? null : `src/${normalized.slice(markerIndex + frontendMarker.length)}`; + }; + const uncoveredCount = (values) => values.filter((value) => value === 0).length; + const frontend_uncovered_line_numbers = (file) => { + const lines = new Set(); + for (const [statementId, count] of Object.entries(file?.s ?? {})) { + if (count !== 0) continue; + const line = file?.statementMap?.[statementId]?.start?.line; + if (Number.isSafeInteger(line) && line > 0) lines.add(line); + } + return [...lines].sort((a, b) => a - b).slice(0, 40); + }; + const frontend_top_uncovered_files = Object.entries(finalCoverage) + .map(([filename, file]) => { + const path = frontendRepositoryPath(filename); + if (!path || !file || typeof file !== 'object') return null; + const branches = Object.values(file.b ?? {}).flatMap((value) => Array.isArray(value) ? value : []); + return { + path, + uncovered_statements: uncoveredCount(Object.values(file.s ?? {})), + uncovered_branches: uncoveredCount(branches), + uncovered_functions: uncoveredCount(Object.values(file.f ?? {})), + uncovered_line_numbers: frontend_uncovered_line_numbers(file), + }; + }) + .filter((entry) => entry && (entry.uncovered_statements || entry.uncovered_branches || entry.uncovered_functions || entry.uncovered_line_numbers.length)) + .sort((a, b) => (b.uncovered_statements + b.uncovered_branches + b.uncovered_functions + b.uncovered_line_numbers.length) - (a.uncovered_statements + a.uncovered_branches + a.uncovered_functions + a.uncovered_line_numbers.length) || a.path.localeCompare(b.path)) + .slice(0, 20); + const diagnostic = { schema_version: 1, head_sha: sha, repository, totals: summaryCoverage?.total ?? null, frontend_top_uncovered_files }; + writeFileSync('frontend-coverage-diagnostic.json', `${JSON.stringify(diagnostic, null, 2)}\n`); + console.error(`frontend-coverage-diagnostic=${JSON.stringify(diagnostic)}`); + if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `### Frontend coverage diagnostic for \`${sha}\`\n\n\`\`\`json\n${JSON.stringify(diagnostic, null, 2)}\n\`\`\`\n`); + NODE + - name: Upload bounded frontend coverage diagnostic + if: failure() && steps.frontend-coverage.outcome == 'failure' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: frontend-coverage-diagnostic-${{ env.HEAD_SHA }} + path: frontend-coverage-diagnostic.json + if-no-files-found: error + - run: npm run check - run: npm run build windows-home-resolution: @@ -74,6 +137,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - name: Windows absolute-home regression shell: pwsh @@ -82,6 +146,117 @@ jobs: rustc --edition=2021 --test src-tauri/tests/home_resolution_contract.rs -o target/home-resolution-contract.exe & .\target\home-resolution-contract.exe + coverage-evidence: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + RUST_TEST_THREADS: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Install Tauri system deps + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev lsof + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: nightly-2026-08-07 + components: llvm-tools-preview + - uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad + with: + tool: cargo-llvm-cov + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + workspaces: src-tauri + cache-targets: false + - name: Measure exact-head Rust coverage + id: rust-coverage + run: | + set +e + cargo llvm-cov --locked --no-cfg-coverage --no-cfg-coverage-nightly --all-features --manifest-path src-tauri/Cargo.toml --branch --json --output-path coverage.json >coverage-command.raw.log 2>&1 + coverage_status=$? + bash .github/scripts/bound-coverage-command-diagnostic.sh coverage-command.raw.log coverage-command.bounded.log + diagnostic_status=$? + node --input-type=module <<'NODE' + import { readFileSync, writeFileSync } from 'node:fs'; + let diagnostic = readFileSync('coverage-command.bounded.log', 'utf8'); + const workspace = process.env.GITHUB_WORKSPACE ?? ''; + const home = process.env.HOME ?? ''; + const runnerTemp = process.env.RUNNER_TEMP ?? ''; + if (workspace) diagnostic = diagnostic.replaceAll(workspace, ''); + if (home) diagnostic = diagnostic.replaceAll(home, ''); + if (runnerTemp) diagnostic = diagnostic.replaceAll(runnerTemp, ''); + writeFileSync('coverage-command-diagnostic.log', diagnostic); + NODE + redaction_status=$? + rm -f coverage-command.raw.log coverage-command.bounded.log + set -e + if [ "$diagnostic_status" -ne 0 ] || [ "$redaction_status" -ne 0 ]; then + printf '%s\n' 'coverage diagnostic rendering failed; raw diagnostic withheld' > coverage-command-diagnostic.log + echo "::warning::coverage diagnostic rendering failed; preserving authoritative coverage exit status" >&2 + fi + exit "$coverage_status" + - name: Upload bounded coverage command diagnostic + if: failure() && steps.rust-coverage.outcome == 'failure' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-command-diagnostic-${{ env.HEAD_SHA }} + path: coverage-command-diagnostic.log + if-no-files-found: error + - name: Build exact-head coverage evidence + run: | + node --input-type=module <<'NODE' + import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; + const sha = process.env.HEAD_SHA ?? ''; + const repository = process.env.GITHUB_REPOSITORY ?? ''; + if (!/^[0-9a-f]{40}$/.test(sha) || repository.length === 0) throw new Error('coverage evidence identity is invalid'); + const report = JSON.parse(readFileSync('coverage.json', 'utf8')); + const coverageData = report?.data?.[0]; + const totals = coverageData?.totals; + const gap = (value) => Number.isSafeInteger(value?.count) && Number.isSafeInteger(value?.covered) && value.count >= value.covered ? value.count - value.covered : 0; + const repositoryPath = (filename) => { + const normalized = String(filename ?? '').replaceAll('\\', '/'); + const marker = '/src-tauri/'; + const markerIndex = normalized.lastIndexOf(marker); + return markerIndex < 0 ? null : `src-tauri/${normalized.slice(markerIndex + marker.length)}`; + }; + const top_uncovered_files = (coverageData?.files ?? []) + .map((file) => { + const path = repositoryPath(file?.filename); + if (!path) return null; + return { path, uncovered_regions: gap(file?.summary?.regions), uncovered_branches: gap(file?.summary?.branches), uncovered_functions: gap(file?.summary?.functions), uncovered_lines: gap(file?.summary?.lines) }; + }) + .filter((entry) => entry && (entry.uncovered_regions || entry.uncovered_branches || entry.uncovered_functions || entry.uncovered_lines)) + .sort((a, b) => (b.uncovered_regions + b.uncovered_branches + b.uncovered_functions + b.uncovered_lines) - (a.uncovered_regions + a.uncovered_branches + a.uncovered_functions + a.uncovered_lines) || a.path.localeCompare(b.path)) + .slice(0, 20); + const diagnostic = { schema_version: 1, head_sha: sha, repository, regions: totals?.regions ?? null, branches: totals?.branches ?? null, functions: totals?.functions ?? null, lines: totals?.lines ?? null, top_uncovered_files }; + writeFileSync('coverage-diagnostic.json', `${JSON.stringify(diagnostic, null, 2)}\n`); + console.error(`coverage-diagnostic=${JSON.stringify(diagnostic)}`); + if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `### Coverage diagnostic for \`${sha}\`\n\n\`\`\`json\n${JSON.stringify(diagnostic, null, 2)}\n\`\`\`\n`); + const metric = (name, value) => { + if (!value || !Number.isFinite(value.count) || !Number.isFinite(value.covered) || !Number.isFinite(value.percent) || value.count <= 0 || value.covered !== value.count || value.percent !== 100) throw new Error(`${name} coverage is not exactly 100%`); + return value.percent; + }; + const evidence = { schema_version: 1, head_sha: sha, commit_sha: sha, repository, trust_tier: 'ci-verified', ci_server: 'github-actions', ci_workflow: 'Test', coverage_command: 'cargo llvm-cov', statement_coverage: metric('statement/region', totals?.regions), branch_coverage: metric('branch', totals?.branches), function_coverage: metric('function', totals?.functions), line_coverage: metric('line', totals?.lines), passed: true }; + writeFileSync('coverage-evidence.json', `${JSON.stringify(evidence, null, 2)}\n`); + NODE + - name: Upload bounded coverage diagnostic + if: always() && hashFiles('coverage-diagnostic.json') != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-diagnostic-${{ env.HEAD_SHA }} + path: coverage-diagnostic.json + if-no-files-found: error + - name: Upload coverage evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-evidence + path: coverage-evidence.json + if-no-files-found: error + llm-engine-build: runs-on: ubuntu-latest timeout-minutes: 30 @@ -89,6 +264,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install build deps (llama.cpp native + tauri) run: | sudo apt-get update @@ -99,4 +275,4 @@ jobs: workspaces: src-tauri cache-targets: false - name: Build with llm-engine (compiles real llama.cpp CPU + engine.rs FFI) - run: cargo test --manifest-path src-tauri/Cargo.toml --features llm-engine --lib --no-run + run: cargo test --locked --manifest-path src-tauri/Cargo.toml --features llm-engine --lib --no-run From b988db7e7f0b566a163c1d33b9c7e07f93172877 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:07:08 +0900 Subject: [PATCH 06/30] test(ci): require failed coverage metric diagnostics --- src/lib/coverageEvidenceWorkflow.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/coverageEvidenceWorkflow.test.ts b/src/lib/coverageEvidenceWorkflow.test.ts index 8f03d38ad..835e4712e 100644 --- a/src/lib/coverageEvidenceWorkflow.test.ts +++ b/src/lib/coverageEvidenceWorkflow.test.ts @@ -67,6 +67,19 @@ describe('Test workflow coverage evidence contract', () => { expect(workflow).toContain('lines: totals?.lines ?? null'); }); + it('runs metric diagnostic construction after a failed coverage measurement when coverage JSON exists', () => { + const buildStart = workflow.indexOf('name: Build exact-head coverage evidence'); + const diagnosticUploadStart = workflow.indexOf( + 'name: Upload bounded coverage diagnostic', + buildStart, + ); + + expect(buildStart).toBeGreaterThanOrEqual(0); + expect(diagnosticUploadStart).toBeGreaterThan(buildStart); + const buildStep = workflow.slice(buildStart, diagnosticUploadStart); + expect(buildStep).toContain("if: always() && hashFiles('coverage.json') != ''"); + }); + it('preserves both ends of a bounded sanitized command diagnostic when measurement itself fails', () => { expect(workflow).toContain('id: rust-coverage'); expect(workflow).toContain('coverage-command.raw.log'); From bb5aedafad309fce927d0b7393d325bfc565889f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:10:38 +0900 Subject: [PATCH 07/30] fix(ci): preserve failed coverage metric diagnostics --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3052cbbfc..cfdd9ba1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -211,6 +211,7 @@ jobs: path: coverage-command-diagnostic.log if-no-files-found: error - name: Build exact-head coverage evidence + if: always() && hashFiles('coverage.json') != '' run: | node --input-type=module <<'NODE' import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; @@ -278,5 +279,3 @@ jobs: with: workspaces: src-tauri cache-targets: false - - name: Build with llm-engine (compiles real llama.cpp CPU + engine.rs FFI) - run: cargo test --locked --manifest-path src-tauri/Cargo.toml --features llm-engine --lib --no-run From 017ea9468a12049e762d1acf0eaefdca25ec27e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 23:30:29 +0900 Subject: [PATCH 08/30] test(ci): require Rust coverage line-target evidence --- src/lib/coverageEvidenceWorkflow.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/coverageEvidenceWorkflow.test.ts b/src/lib/coverageEvidenceWorkflow.test.ts index 835e4712e..0cff68846 100644 --- a/src/lib/coverageEvidenceWorkflow.test.ts +++ b/src/lib/coverageEvidenceWorkflow.test.ts @@ -109,6 +109,9 @@ describe('Test workflow coverage evidence contract', () => { expect(measureStep).toContain('diagnostic_status=$?'); expect(measureStep).toContain('redaction_status=$?'); expect(measureStep).toContain('coverage diagnostic rendering failed'); + expect(measureStep).toContain( + "printf '%s\\n' 'coverage diagnostic rendering failed; raw diagnostic withheld' > coverage-command-diagnostic.log", + ); expect(measureStep).toContain('exit "$coverage_status"'); }); @@ -121,6 +124,14 @@ describe('Test workflow coverage evidence contract', () => { expect(workflow).toContain('uncovered_lines'); }); + it('preserves source-targetable Rust uncovered line numbers from the superseded coverage lineage', () => { + expect(workflow).toContain('uncovered_line_numbers'); + expect(workflow).toContain('const uncoveredLineNumbers = (segments) =>'); + expect(workflow).toContain('Array.isArray(segment)'); + expect(workflow).toContain('segment[2] === 0'); + expect(workflow).toContain('.slice(0, 40)'); + }); + it('preserves bounded frontend diagnostics when the all-production threshold fails', () => { expect(workflow).toContain('name: Build bounded frontend coverage diagnostic'); expect(workflow).toContain("readFileSync('coverage/coverage-final.json', 'utf8')"); @@ -138,4 +149,4 @@ describe('Test workflow coverage evidence contract', () => { expect(workflow).toContain('function_coverage'); expect(workflow).toContain('line_coverage'); }); -}); +}); \ No newline at end of file From cbe6abab6d0405156ee2f704884dbad6fe2c453c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 23:32:33 +0900 Subject: [PATCH 09/30] fix(ci): preserve Rust coverage line targets --- .github/workflows/test.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cfdd9ba1a..282c215cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -228,11 +228,35 @@ jobs: const markerIndex = normalized.lastIndexOf(marker); return markerIndex < 0 ? null : `src-tauri/${normalized.slice(markerIndex + marker.length)}`; }; + const uncoveredLineNumbers = (segments) => { + const lines = new Set(); + for (const segment of Array.isArray(segments) ? segments : []) { + if ( + Array.isArray(segment) && + Number.isSafeInteger(segment[0]) && + segment[0] > 0 && + segment[2] === 0 && + segment[3] === true && + segment[4] === true && + segment[5] !== true + ) { + lines.add(segment[0]); + } + } + return [...lines].sort((a, b) => a - b).slice(0, 40); + }; const top_uncovered_files = (coverageData?.files ?? []) .map((file) => { const path = repositoryPath(file?.filename); if (!path) return null; - return { path, uncovered_regions: gap(file?.summary?.regions), uncovered_branches: gap(file?.summary?.branches), uncovered_functions: gap(file?.summary?.functions), uncovered_lines: gap(file?.summary?.lines) }; + return { + path, + uncovered_regions: gap(file?.summary?.regions), + uncovered_branches: gap(file?.summary?.branches), + uncovered_functions: gap(file?.summary?.functions), + uncovered_lines: gap(file?.summary?.lines), + uncovered_line_numbers: uncoveredLineNumbers(file?.segments), + }; }) .filter((entry) => entry && (entry.uncovered_regions || entry.uncovered_branches || entry.uncovered_functions || entry.uncovered_lines)) .sort((a, b) => (b.uncovered_regions + b.uncovered_branches + b.uncovered_functions + b.uncovered_lines) - (a.uncovered_regions + a.uncovered_branches + a.uncovered_functions + a.uncovered_lines) || a.path.localeCompare(b.path)) From 75254c6fdde909ed9e7f462d8baff84f52bb9b1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 23:33:54 +0900 Subject: [PATCH 10/30] test(ci): inherit coverage diagnostic contracts --- src/lib/coverageEvidenceWorkflow.test.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/lib/coverageEvidenceWorkflow.test.ts b/src/lib/coverageEvidenceWorkflow.test.ts index 0cff68846..ebce00d78 100644 --- a/src/lib/coverageEvidenceWorkflow.test.ts +++ b/src/lib/coverageEvidenceWorkflow.test.ts @@ -89,11 +89,15 @@ describe('Test workflow coverage evidence contract', () => { expect(diagnosticHelper).toContain('edge_bytes=9000'); expect(diagnosticHelper).toContain('head -c "$edge_bytes" "$line_bounded_log"'); expect(diagnosticHelper).toContain('tail -c "$edge_bytes" "$line_bounded_log"'); + expect(diagnosticHelper).toContain('--- bounded diagnostic tail ---'); expect(workflow).toContain("replaceAll(workspace, '')"); expect(workflow).toContain("replaceAll(home, '')"); expect(workflow).toContain( "if: failure() && steps.rust-coverage.outcome == 'failure'", ); + expect(workflow).toContain( + 'name: coverage-command-diagnostic-${{ env.HEAD_SHA }}', + ); }); it('preserves the authoritative coverage exit status when diagnostic rendering fails', () => { @@ -113,11 +117,17 @@ describe('Test workflow coverage evidence contract', () => { "printf '%s\\n' 'coverage diagnostic rendering failed; raw diagnostic withheld' > coverage-command-diagnostic.log", ); expect(measureStep).toContain('exit "$coverage_status"'); + expect(measureStep.indexOf('set -e')).toBeGreaterThan( + measureStep.indexOf('redaction_status=$?'), + ); }); it('identifies the largest exact-head Rust coverage gaps without leaking runner paths', () => { expect(workflow).toContain('top_uncovered_files'); expect(workflow).toContain("const marker = '/src-tauri/'"); + expect(workflow).toContain( + 'return markerIndex < 0 ? null : `src-tauri/${normalized.slice(markerIndex + marker.length)}`;', + ); expect(workflow).toContain('uncovered_regions'); expect(workflow).toContain('uncovered_branches'); expect(workflow).toContain('uncovered_functions'); @@ -132,12 +142,28 @@ describe('Test workflow coverage evidence contract', () => { expect(workflow).toContain('.slice(0, 40)'); }); + it('surfaces the same bounded Rust diagnostic in logs and the GitHub step summary', () => { + expect(workflow).toContain( + 'console.error(`coverage-diagnostic=${JSON.stringify(diagnostic)}`)', + ); + expect(workflow).toContain('process.env.GITHUB_STEP_SUMMARY'); + expect(workflow).toContain('Coverage diagnostic for \\`${sha}\\`'); + }); + it('preserves bounded frontend diagnostics when the all-production threshold fails', () => { expect(workflow).toContain('name: Build bounded frontend coverage diagnostic'); expect(workflow).toContain("readFileSync('coverage/coverage-final.json', 'utf8')"); expect(workflow).toContain("readFileSync('coverage/coverage-summary.json', 'utf8')"); expect(workflow).toContain('frontend_top_uncovered_files'); + expect(workflow).toContain('frontend_uncovered_line_numbers'); expect(workflow).toContain('frontend-coverage-diagnostic.json'); + expect(workflow).toContain( + 'name: frontend-coverage-diagnostic-${{ env.HEAD_SHA }}', + ); + expect(workflow).toContain('path: frontend-coverage-diagnostic.json'); + expect(workflow).toContain( + "if: failure() && steps.frontend-coverage.outcome == 'failure'", + ); }); it('uploads fail-closed evidence under the organization contract name', () => { From 52b79da21e7c703a912b6bca728b9a36ae01f48f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:02:30 +0900 Subject: [PATCH 11/30] test(ci): inherit executable coverage diagnostic contract --- .../coverageCommandDiagnosticBound.test.ts | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 src/lib/coverageCommandDiagnosticBound.test.ts diff --git a/src/lib/coverageCommandDiagnosticBound.test.ts b/src/lib/coverageCommandDiagnosticBound.test.ts new file mode 100644 index 000000000..481fe35ab --- /dev/null +++ b/src/lib/coverageCommandDiagnosticBound.test.ts @@ -0,0 +1,185 @@ +import { execFileSync } from 'node:child_process'; +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync( + new URL('../../.github/workflows/test.yml', import.meta.url), + 'utf8', +); +const diagnosticHelper = fileURLToPath( + new URL('../../.github/scripts/bound-coverage-command-diagnostic.sh', import.meta.url), +); + +describe('bounded Rust coverage command diagnostic', () => { + it('preserves a compiler diagnostic after an oversized rustc invocation', () => { + const directory = mkdtempSync(join(tmpdir(), 'disksage-coverage-diagnostic-')); + try { + const rawLog = join(directory, 'raw.log'); + const boundedLog = join(directory, 'bounded.log'); + const compilerDiagnostic = + 'error[E0308]: mismatched types\n --> src-tauri/tests/example.rs:41:7\n'; + writeFileSync( + rawLog, + `${'rustc --crate-name disksage '.padEnd(50_000, 'x')}\n${compilerDiagnostic}`, + ); + + execFileSync('bash', [diagnosticHelper, rawLog, boundedLog]); + + const diagnostic = readFileSync(boundedLog, 'utf8'); + expect(diagnostic).toContain(' ... [line truncated]'); + expect(diagnostic).toContain(compilerDiagnostic); + expect(Buffer.byteLength(diagnostic)).toBeLessThan(5_000); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it('preserves compiler diagnostics that fall between long bounded log edges', () => { + const directory = mkdtempSync(join(tmpdir(), 'disksage-coverage-diagnostic-middle-')); + try { + const rawLog = join(directory, 'raw.log'); + const boundedLog = join(directory, 'bounded.log'); + const compilerDiagnostic = + 'error[E0753]: expected outer doc comment\n --> src-tauri/src/bin/example.rs:1:1\n'; + const prefixNoise = Array.from( + { length: 500 }, + (_, index) => `prefix-noise-${String(index).padStart(4, '0')} ${'x'.repeat(80)}`, + ).join('\n'); + const suffixNoise = Array.from( + { length: 500 }, + (_, index) => `suffix-noise-${String(index).padStart(4, '0')} ${'y'.repeat(80)}`, + ).join('\n'); + writeFileSync( + rawLog, + `${prefixNoise}\n${compilerDiagnostic}${suffixNoise}\n`, + ); + + execFileSync('bash', [diagnosticHelper, rawLog, boundedLog]); + + const diagnostic = readFileSync(boundedLog, 'utf8'); + expect(diagnostic).toContain(compilerDiagnostic); + expect(Buffer.byteLength(diagnostic)).toBeLessThanOrEqual(32_768); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it('prioritizes compiler errors when earlier warnings exhaust the focus budget', () => { + const directory = mkdtempSync(join(tmpdir(), 'disksage-coverage-diagnostic-warnings-')); + try { + const rawLog = join(directory, 'raw.log'); + const boundedLog = join(directory, 'bounded.log'); + const compilerDiagnostic = + 'error[E0425]: cannot find value `missing` in this scope\n --> src-tauri/src/lib.rs:777:9\n'; + const prefixNoise = Array.from( + { length: 180 }, + (_, index) => `prefix-noise-${String(index).padStart(4, '0')} ${'x'.repeat(80)}`, + ).join('\n'); + const warnings = Array.from( + { length: 220 }, + (_, index) => + `warning: pre-error warning ${String(index).padStart(4, '0')} ${'w'.repeat(70)}\n` + + ` --> src-tauri/src/warn${String(index).padStart(4, '0')}.rs:1:1`, + ).join('\n'); + const suffixNoise = Array.from( + { length: 500 }, + (_, index) => `suffix-noise-${String(index).padStart(4, '0')} ${'y'.repeat(80)}`, + ).join('\n'); + writeFileSync( + rawLog, + `${prefixNoise}\n${warnings}\n${compilerDiagnostic}${suffixNoise}\n`, + ); + + execFileSync('bash', [diagnosticHelper, rawLog, boundedLog]); + + const diagnostic = readFileSync(boundedLog, 'utf8'); + expect(diagnostic).toContain(compilerDiagnostic); + expect(Buffer.byteLength(diagnostic)).toBeLessThanOrEqual(32_768); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it('preserves ANSI-colored Rust errors emitted by the coverage runner', () => { + const directory = mkdtempSync(join(tmpdir(), 'disksage-coverage-diagnostic-ansi-')); + try { + const rawLog = join(directory, 'raw.log'); + const boundedLog = join(directory, 'bounded.log'); + const compilerDiagnostic = + '\u001b[31merror[E0425]\u001b[0m: cannot find value `missing` in this scope\n' + + '\u001b[34m --> \u001b[0msrc-tauri/src/lib.rs:777:9\n'; + const prefixNoise = Array.from( + { length: 500 }, + (_, index) => `prefix-noise-${String(index).padStart(4, '0')} ${'x'.repeat(80)}`, + ).join('\n'); + const suffixNoise = Array.from( + { length: 500 }, + (_, index) => `suffix-noise-${String(index).padStart(4, '0')} ${'y'.repeat(80)}`, + ).join('\n'); + writeFileSync( + rawLog, + `${prefixNoise}\n${compilerDiagnostic}${suffixNoise}\n`, + ); + + execFileSync('bash', [diagnosticHelper, rawLog, boundedLog]); + + const diagnostic = readFileSync(boundedLog, 'utf8'); + expect(diagnostic).toContain('error[E0425]'); + expect(diagnostic).toContain('src-tauri/src/lib.rs:777:9'); + expect(Buffer.byteLength(diagnostic)).toBeLessThanOrEqual(32_768); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it('preserves Rust test panic context from the middle of an oversized log', () => { + const directory = mkdtempSync(join(tmpdir(), 'disksage-coverage-diagnostic-panic-')); + try { + const rawLog = join(directory, 'raw.log'); + const boundedLog = join(directory, 'bounded.log'); + const panicDiagnostic = + "thread 'provider_oauth::tests::oauth_connection_document_bounds_and_links_fail_closed' panicked at src-tauri/src/provider_oauth.rs:1401:9:\n" + + 'assertion `left == right` failed\n' + + ' left: "oauth-connection-document-permissions-unsafe"\n' + + ' right: "oauth-connection-document-too-large"\n' + + 'note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n'; + const prefixNoise = Array.from( + { length: 500 }, + (_, index) => `prefix-noise-${String(index).padStart(4, '0')} ${'x'.repeat(80)}`, + ).join('\n'); + const suffixNoise = Array.from( + { length: 500 }, + (_, index) => `suffix-noise-${String(index).padStart(4, '0')} ${'y'.repeat(80)}`, + ).join('\n'); + writeFileSync(rawLog, `${prefixNoise}\n${panicDiagnostic}${suffixNoise}\n`); + + execFileSync('bash', [diagnosticHelper, rawLog, boundedLog]); + + const diagnostic = readFileSync(boundedLog, 'utf8'); + expect(diagnostic).toContain("thread 'provider_oauth::tests::oauth_connection_document_bounds_and_links_fail_closed' panicked at"); + expect(diagnostic).toContain('assertion `left == right` failed'); + expect(diagnostic).toContain('left: "oauth-connection-document-permissions-unsafe"'); + expect(diagnostic).toContain('right: "oauth-connection-document-too-large"'); + expect(Buffer.byteLength(diagnostic)).toBeLessThanOrEqual(32_768); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it('wires the exact coverage step through the executable helper', () => { + expect(workflow).toContain( + 'bash .github/scripts/bound-coverage-command-diagnostic.sh coverage-command.raw.log coverage-command.bounded.log', + ); + expect(workflow).toContain( + 'rm -f coverage-command.raw.log coverage-command.bounded.log', + ); + }); +}); From c7d75c788b3c065093b172f51e2032e22da4e252 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:04:11 +0900 Subject: [PATCH 12/30] test(ci): inherit hosted runner disk budget contract --- src-tauri/tests/test_workflow_disk_budget.rs | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src-tauri/tests/test_workflow_disk_budget.rs diff --git a/src-tauri/tests/test_workflow_disk_budget.rs b/src-tauri/tests/test_workflow_disk_budget.rs new file mode 100644 index 000000000..e89987364 --- /dev/null +++ b/src-tauri/tests/test_workflow_disk_budget.rs @@ -0,0 +1,101 @@ +//! Regression contracts for bounded GitHub-hosted runner disk and linker usage. +//! +//! The ordinary `Test` job deliberately compiles several large Rust feature +//! combinations before it reaches the frontend checks. Real exact-head runs +//! have exhausted hosted-runner resources while entering later feature +//! batches. These tests keep the repair reviewable: the workflow must release +//! reusable Cargo build space between large batches and must not compile every +//! integration-test target merely to exercise a focused feature-gated library +//! surface. + +use std::fs; +use std::path::PathBuf; + +fn workflow_source() -> String { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workflow_path = manifest_dir + .parent() + .expect("src-tauri must have a repository parent") + .join(".github/workflows/test.yml"); + fs::read_to_string(workflow_path).expect("Test workflow must be readable") +} + +#[test] +fn test_job_reclaims_rust_build_space_before_archive_feature_batch() { + let workflow = workflow_source(); + let duplicate = workflow + .find("- name: Exact duplicate audit tests") + .expect("duplicate-audit batch must remain in the Test job"); + let reclaim = workflow + .find("- name: Reclaim Rust test build space before archive proofs") + .expect("Test job must reclaim Rust build space before archive proofs"); + let archive = workflow + .find("- name: Extraction-free archive tree proof tests") + .expect("archive proof batch must remain in the Test job"); + + assert!( + duplicate < reclaim && reclaim < archive, + "disk reclamation must occur after duplicate tests and before archive tests" + ); + + let reclaim_block = &workflow[reclaim..archive]; + assert!( + reclaim_block.contains("cargo clean --manifest-path src-tauri/Cargo.toml"), + "reclamation must remove only Cargo build artifacts through cargo clean" + ); + assert!( + reclaim_block.contains("df -h ."), + "reclamation must leave bounded disk-availability evidence in the job log" + ); +} + +#[test] +fn duplicate_audit_feature_batch_builds_only_the_intended_library_and_cli_targets() { + let workflow = workflow_source(); + let duplicate = workflow + .find("- name: Exact duplicate audit tests") + .expect("duplicate-audit batch must remain in the Test job"); + let reclaim = workflow + .find("- name: Reclaim Rust test build space before archive proofs") + .expect("duplicate-audit batch must remain bounded before reclamation"); + let duplicate_block = &workflow[duplicate..reclaim]; + + assert!( + duplicate_block.contains( + "cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --features cloud-cli duplicate_audit" + ), + "the cloud-cli duplicate-audit library proof must stay lockfile-bound and use --lib so Cargo does not relink every integration-test target" + ); + assert!( + duplicate_block.contains( + "cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-duplicate-audit" + ), + "the dedicated duplicate-audit CLI proof must remain explicit and lockfile-bound" + ); +} + +#[test] +fn archive_feature_batch_builds_only_the_intended_library_and_cli_targets() { + let workflow = workflow_source(); + let archive = workflow + .find("- name: Extraction-free archive tree proof tests") + .expect("archive proof batch must remain in the Test job"); + let node_setup = workflow[archive..] + .find("- uses: actions/setup-node@") + .map(|offset| archive + offset) + .expect("Node setup must remain after the archive proof batch"); + let archive_block = &workflow[archive..node_setup]; + + assert!( + archive_block.contains( + "cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --features archive-cli archive_git_tree" + ), + "the archive-cli library proof must stay lockfile-bound and use --lib so Cargo does not relink every integration-test target" + ); + assert!( + archive_block.contains( + "cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli --bin disksage-archive-tree" + ), + "the dedicated archive-tree CLI proof must remain explicit and lockfile-bound" + ); +} From ff0a6f2f655eba49fa3dbbfcd79b838be8cd0002 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:05:40 +0900 Subject: [PATCH 13/30] fix(ci): bound Rust feature-batch disk and linker use --- .github/workflows/test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 282c215cf..3342cf31a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,11 +59,15 @@ jobs: run: cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-cloud-plan - name: Exact duplicate audit tests run: | - cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli duplicate_audit + cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --features cloud-cli duplicate_audit cargo test --locked --manifest-path src-tauri/Cargo.toml --features cloud-cli --bin disksage-duplicate-audit + - name: Reclaim Rust test build space before archive proofs + run: | + cargo clean --manifest-path src-tauri/Cargo.toml + df -h . - name: Extraction-free archive tree proof tests run: | - cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli archive_git_tree + cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --features archive-cli archive_git_tree cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli --bin disksage-archive-tree cargo test --locked --manifest-path src-tauri/Cargo.toml --features archive-cli --test archive_tree_help_exit - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 From b6fa431034e9aa91bc7bdb537fd8892415f2b510 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:06:37 +0900 Subject: [PATCH 14/30] test(ci): inherit Rust evidence concurrency contract --- src/lib/rustTestConcurrencyWorkflow.test.ts | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/lib/rustTestConcurrencyWorkflow.test.ts diff --git a/src/lib/rustTestConcurrencyWorkflow.test.ts b/src/lib/rustTestConcurrencyWorkflow.test.ts new file mode 100644 index 000000000..5fe937821 --- /dev/null +++ b/src/lib/rustTestConcurrencyWorkflow.test.ts @@ -0,0 +1,46 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync( + new URL('../../.github/workflows/test.yml', import.meta.url), + 'utf8', +); + +const jobBody = (name: string, nextName: string): string => { + const start = ` ${name}:\n`; + const end = `\n ${nextName}:\n`; + const startIndex = workflow.indexOf(start); + const endIndex = workflow.indexOf(end, startIndex + start.length); + expect(startIndex, `${name} job must exist`).toBeGreaterThanOrEqual(0); + expect(endIndex, `${nextName} job must follow ${name}`).toBeGreaterThan(startIndex); + return workflow.slice(startIndex, endIndex); +}; + +describe('native Rust test concurrency contract', () => { + it('serializes host-global active-use evidence probes in every test-executing Rust job', () => { + const nativeTestJob = jobBody('test', 'windows-home-resolution'); + const coverageJob = jobBody('coverage-evidence', 'llm-engine-build'); + + expect(nativeTestJob).toContain('RUST_TEST_THREADS: "1"'); + expect(coverageJob).toContain('RUST_TEST_THREADS: "1"'); + expect(workflow.split('RUST_TEST_THREADS: "1"').length - 1).toBe(2); + }); + + it('does not restore compiled target artifacts into exact-head Rust evidence jobs', () => { + const rustCacheUses = workflow.split('uses: Swatinem/rust-cache@').length - 1; + const registryOnlyCaches = workflow.split('cache-targets: false').length - 1; + + expect(rustCacheUses).toBeGreaterThan(0); + expect(registryOnlyCaches).toBe(rustCacheUses); + }); + + it('keeps exact-head identity and exact coverage thresholds unchanged while serializing tests', () => { + expect(workflow).toContain( + 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}', + ); + expect(workflow).toContain( + 'cargo llvm-cov --locked --no-cfg-coverage --no-cfg-coverage-nightly --all-features --manifest-path src-tauri/Cargo.toml --branch --json --output-path coverage.json', + ); + expect(workflow).toContain('value.percent !== 100'); + }); +}); From f68c150a7ab688c53cc5753ded8fd346dde208fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:07:18 +0900 Subject: [PATCH 15/30] test(ci): reproduce stale-head Test queue retention --- .../testWorkflowConcurrencyContract.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/lib/testWorkflowConcurrencyContract.test.ts diff --git a/src/lib/testWorkflowConcurrencyContract.test.ts b/src/lib/testWorkflowConcurrencyContract.test.ts new file mode 100644 index 000000000..71a8a8dfe --- /dev/null +++ b/src/lib/testWorkflowConcurrencyContract.test.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +function readTestWorkflow(): string { + return readFileSync(resolve(repositoryRoot, '.github/workflows/test.yml'), 'utf8').replace( + /\r\n?/g, + '\n', + ); +} + +describe('Test workflow supersession', () => { + it('cancels obsolete first-attempt work for one ref without making reruns self-cancel', () => { + const workflow = readTestWorkflow(); + const concurrencyStart = workflow.indexOf('concurrency:'); + const permissionsStart = workflow.indexOf('permissions:'); + + expect(concurrencyStart).toBeGreaterThanOrEqual(0); + expect(permissionsStart).toBeGreaterThan(concurrencyStart); + + const concurrencyBlock = workflow.slice(concurrencyStart, permissionsStart); + expect(concurrencyBlock).toContain( + 'group: test-${{ github.workflow }}-${{ github.ref }}', + ); + expect(concurrencyBlock).toContain( + 'cancel-in-progress: ${{ github.run_attempt == 1 }}', + ); + expect(concurrencyBlock).not.toContain('github.sha'); + expect(concurrencyBlock).not.toContain('pull_request.head.sha'); + }); + + it('keeps every native Test job explicitly time-bounded', () => { + const workflow = readTestWorkflow(); + + expect(workflow).toContain( + ' test:\n runs-on: ubuntu-latest\n timeout-minutes: 60\n', + ); + expect(workflow).toContain( + ' windows-home-resolution:\n runs-on: windows-latest\n timeout-minutes: 10\n', + ); + expect(workflow).toContain( + ' coverage-evidence:\n runs-on: ubuntu-latest\n timeout-minutes: 60\n', + ); + expect(workflow).toContain( + ' llm-engine-build:\n runs-on: ubuntu-latest\n timeout-minutes: 30\n', + ); + }); +}); From 58834dd40b6d4943073d78f8e1b4f188457b65d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:07:54 +0900 Subject: [PATCH 16/30] fix(ci): cancel obsolete first-attempt Test runs --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3342cf31a..5f2d4731d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,10 @@ on: - "docs/architecture/goals/cloud-offload-goal.json" - "CHANGELOG.md" +concurrency: + group: test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.run_attempt == 1 }} + permissions: contents: read From d5513be67e67fd61eabc53c80726a2a458d6a7c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:09:11 +0900 Subject: [PATCH 17/30] docs(ci): restore current exact-head coverage evidence contract --- docs/development/coverage-evidence.md | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/development/coverage-evidence.md diff --git a/docs/development/coverage-evidence.md b/docs/development/coverage-evidence.md new file mode 100644 index 000000000..eca6db78c --- /dev/null +++ b/docs/development/coverage-evidence.md @@ -0,0 +1,87 @@ +# Exact-head coverage evidence + +DiskSage treats coverage as CI evidence bound to one immutable source head. A locally reported percentage, a predecessor workflow run, or a successful test exit code is not equivalent evidence. + +## Exact source identity + +The `Test` workflow checks out `${{ github.event.pull_request.head.sha || github.sha }}` explicitly in every checkout-bearing job. The coverage job copies the same value into `HEAD_SHA`, validates it as a 40-character commit SHA, and records it as both `head_sha` and `commit_sha` in `coverage-evidence.json`. + +This deliberately distinguishes the pull-request source head from GitHub's synthetic merge commit and from the PR's historical base snapshot. A coverage artifact is valid only for the exact head that produced it. + +## Rust measurement boundary + +The Rust evidence job uses a dated nightly toolchain with `llvm-tools-preview` and runs: + +```text +cargo llvm-cov --locked --no-cfg-coverage --no-cfg-coverage-nightly --all-features --manifest-path src-tauri/Cargo.toml --branch --json --output-path coverage.json +``` + +`--branch` is explicit because branch coverage is part of the repository gate. The dated nightly is intentional because cargo-llvm-cov documents branch coverage as unstable/nightly-dependent. + +`--no-cfg-coverage` and `--no-cfg-coverage-nightly` are also intentional. Instrumentation must not silently alter DiskSage production `cfg` semantics and thereby change the code graph being claimed by the gate. Ordinary exact-head Rust tests and the feature-specific CLI/library proofs exercise effectful boundaries separately. + +The JSON report is the sole source for emitted percentages. The evidence builder requires non-empty, finite, fully covered totals and exactly 100% for: + +- statement-equivalent LLVM region coverage; +- branch coverage; +- function coverage; and +- line coverage. + +Missing or malformed totals, zero denominators, partial coverage, or identity drift prevent `coverage-evidence.json` from being produced as passing evidence. + +## Failure evidence + +Coverage failure must remain actionable without leaking runner-local paths or unbounded command output. + +If `cargo llvm-cov` produces `coverage.json` but exits because a metric is below the required threshold, `Build exact-head coverage evidence` still runs under `always() && hashFiles('coverage.json') != ''`. It writes `coverage-diagnostic.json` with repository-relative high-gap files and up to 40 sorted uncovered line numbers per file, then the exact-100% validation fails closed before the success artifact can be emitted. + +If measurement itself fails before usable JSON exists, `.github/scripts/bound-coverage-command-diagnostic.sh` produces a bounded command diagnostic. The helper caps pathological individual lines, preserves both bounded log edges, prioritizes compiler errors and test panic context, and retains ANSI-colored Rust diagnostics after normalization. The workflow removes raw transient logs after redaction. If diagnostic rendering or redaction fails, the authoritative coverage exit status is preserved and the only replacement text is: + +```text +coverage diagnostic rendering failed; raw diagnostic withheld +``` + +The same bounded diagnostic identity is carried in the artifact name with the exact head SHA. Failure diagnostics are not passing coverage evidence. + +## Frontend scope + +Vitest coverage includes source-controlled production TypeScript under `src/lib/**/*.ts` and `src/routes/**/*.ts`, excluding tests and generated declaration files. It does not use a hand-maintained production-file allowlist. Statement, branch, function, and line thresholds remain exactly 100%. + +A frontend failure produces a bounded diagnostic with repository-relative file identity and uncovered line coordinates. It does not relax the threshold or remove production files from the denominator. + +## Hosted-runner resource contract + +The ordinary Test job contains several large Rust feature batches. To avoid treating hosted-runner disk/linker exhaustion as a product defect while still preserving the actual proofs: + +- duplicate-audit and archive library checks use `--lib` so Cargo does not relink unrelated integration-test targets for a focused library proof; +- their dedicated CLI proofs remain explicit and `--locked`; +- `cargo clean --manifest-path src-tauri/Cargo.toml` reclaims disposable Cargo build artifacts between the duplicate-audit and archive batches; and +- `df -h .` leaves bounded disk-availability evidence in the workflow log. + +The cleanup does not delete source, lockfiles, coverage thresholds, or test contracts. + +## Concurrency and stale-head runs + +Repeated commits to one pull-request branch can otherwise leave obsolete Test runs queued while a newer exact head becomes authoritative. The workflow therefore uses a same-ref concurrency group: + +```yaml +concurrency: + group: test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.run_attempt == 1 }} +``` + +The group intentionally omits source SHA so a newer first-attempt run can supersede older first-attempt work for the same ref. A manual or automated rerun (`run_attempt > 1`) is not made to self-cancel by this condition. Canceled or superseded runs remain non-passing; only the unchanged current head can supply merge evidence. + +GitHub documents concurrency groups as the mechanism for limiting simultaneous workflow/job execution and canceling outdated runs. It also documents workflow artifacts as the mechanism for persisting outputs such as test and coverage results after a job completes. + +## Operating rule + +A missing `coverage-evidence` artifact is not passing. Queued, pending, canceled, failed, stale-head, malformed, less-than-100%, predecessor, or synthetic-merge evidence is non-passing. Engineers must add realistic tests or remove genuinely unreachable production code; they must not lower thresholds, hard-code percentages, narrow the production denominator, or reuse an artifact from a different head. + +## References + +GitHub. (2026). *Concurrency*. GitHub Docs. https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency + +GitHub. (2026). *Workflow artifacts*. GitHub Docs. https://docs.github.com/en/actions/concepts/workflows-and-actions/workflow-artifacts + +Endo, T. (2026). *cargo-llvm-cov: Cargo subcommand to easily use LLVM source-based code coverage* [Computer software]. GitHub. https://github.com/taiki-e/cargo-llvm-cov From 3a231dbdc1d8065c0f7f19e0c66f6b42c24ff7f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:09:31 +0900 Subject: [PATCH 18/30] test(ci): require coverage contract docs to trigger Test --- src/lib/coverageEvidenceDocsPathContract.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/lib/coverageEvidenceDocsPathContract.test.ts diff --git a/src/lib/coverageEvidenceDocsPathContract.test.ts b/src/lib/coverageEvidenceDocsPathContract.test.ts new file mode 100644 index 000000000..9b55edede --- /dev/null +++ b/src/lib/coverageEvidenceDocsPathContract.test.ts @@ -0,0 +1,14 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync( + new URL('../../.github/workflows/test.yml', import.meta.url), + 'utf8', +); + +describe('coverage evidence documentation path contract', () => { + it('runs Test when the executable coverage contract documentation changes', () => { + const contractPath = 'docs/development/coverage-evidence.md'; + expect(workflow.split(contractPath).length - 1).toBe(2); + }); +}); From 5ccf354e335d4c38da49faa3214fbb64cde3c11e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:10:11 +0900 Subject: [PATCH 19/30] fix(ci): test coverage contract documentation changes --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f2d4731d..48af422a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ on: - "docs/doctoring/model-artifact-integrity.md" - "docs/doctoring/model-load-handle-binding.md" - "docs/development/icloud-local-eviction-batch.md" + - "docs/development/coverage-evidence.md" - "docs/architecture/goals/cloud-offload-goal.json" - "CHANGELOG.md" pull_request: @@ -26,6 +27,7 @@ on: - "docs/doctoring/model-artifact-integrity.md" - "docs/doctoring/model-load-handle-binding.md" - "docs/development/icloud-local-eviction-batch.md" + - "docs/development/coverage-evidence.md" - "docs/architecture/goals/cloud-offload-goal.json" - "CHANGELOG.md" From af39bce9bb6ac3186e3940e2c94dd8381080f619 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 01:36:10 +0900 Subject: [PATCH 20/30] test: inherit cloud review format-control coverage --- .../cloudReviewQueue.formatControls.test.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/lib/cloudReviewQueue.formatControls.test.ts diff --git a/src/lib/cloudReviewQueue.formatControls.test.ts b/src/lib/cloudReviewQueue.formatControls.test.ts new file mode 100644 index 000000000..39416fc3b --- /dev/null +++ b/src/lib/cloudReviewQueue.formatControls.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import type { CloudCandidate, CloudReviewDecision } from "./api"; +import { matchingReviewDecision } from "./cloudReviewQueue"; + +function candidate(): CloudCandidate { + return { + metadata_fingerprint: "a".repeat(64), + review_fingerprint: "b".repeat(64), + src: "/source/a.pdf", + dst: "/cloud/a.pdf", + provider: "icloud", + destination_account_scope: "personal", + kind: "document", + bytes: 1_024, + age_days: 10, + created_ms: 100, + modified_ms: 200, + production_time_ms: 300, + production_time_source: "filesystem:created", + production_time_confidence: "low", + source_root: "/source", + relative_path: "a.pdf", + source_context: ".", + requires_review: true, + review_reasons: ["personal-cloud-sensitive-context-needs-explicit-approval"], + content_title: null, + content_authors: [], + content_context: [], + duration_ms: null, + dataset_profile: null, + metadata_evidence: [], + blocked_reason: null, + }; +} + +function decision(item: CloudCandidate, rationale: string): CloudReviewDecision { + return { + version: 2, + decision_id: "d".repeat(64), + candidate_fingerprint: item.metadata_fingerprint, + review_fingerprint: item.review_fingerprint, + disposition: "approved", + reviewed_at_ms: 400, + reviewed_by: "human:local:test", + rationale, + }; +} + +describe("cloud review rationale format-control hardening", () => { + it("rejects representative code points from every supported Unicode format-control class", () => { + const item = candidate(); + const representativeCodepoints = [ + 0x00ad, + 0x0600, + 0x061c, + 0x0890, + 0x200c, + 0x202a, + 0x2061, + 0x2066, + 0xfff9, + 0x110bd, + 0x13430, + 0x1bca0, + 0x1d173, + 0xe0020, + ]; + + for (const codepoint of representativeCodepoints) { + const rationale = `metadata${String.fromCodePoint(codepoint)}reviewed`; + expect( + matchingReviewDecision(item, [decision(item, rationale)]), + `U+${codepoint.toString(16).toUpperCase()}`, + ).toBeNull(); + } + }); +}); From c820ab5102f173852df3362245c1611179074b73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:15:16 +0900 Subject: [PATCH 21/30] test(commands): inherit current-compatible coverage evidence --- src-tauri/src/commands_env_coverage_tests.rs | 41 ++ src-tauri/src/commands_public_tests.rs | 421 +++++++++++++++++++ src-tauri/src/lib.rs | 4 + 3 files changed, 466 insertions(+) create mode 100644 src-tauri/src/commands_env_coverage_tests.rs create mode 100644 src-tauri/src/commands_public_tests.rs diff --git a/src-tauri/src/commands_env_coverage_tests.rs b/src-tauri/src/commands_env_coverage_tests.rs new file mode 100644 index 000000000..28d57992a --- /dev/null +++ b/src-tauri/src/commands_env_coverage_tests.rs @@ -0,0 +1,41 @@ +//! Coverage for command-layer environment branches that must remain deterministic and side-effect safe. + +#[cfg(not(windows))] +use crate::commands::list_roots; + +#[cfg(not(windows))] +struct EnvRestore { + key: &'static str, + value: Option, +} + +#[cfg(not(windows))] +impl EnvRestore { + fn remove(key: &'static str) -> Self { + let value = std::env::var_os(key); + std::env::remove_var(key); + Self { key, value } + } +} + +#[cfg(not(windows))] +impl Drop for EnvRestore { + fn drop(&mut self) { + match self.value.take() { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } +} + +#[cfg(not(windows))] +#[test] +fn list_roots_does_not_invent_a_home_root_when_home_is_absent() { + // The Rust test workflow is serialized (`RUST_TEST_THREADS=1`), so temporarily removing HOME + // cannot race another DiskSage unit test. Restore it even if the assertion unwinds. + let _restore = EnvRestore::remove("HOME"); + + let roots = list_roots(); + + assert_eq!(roots, vec!["/".to_string()]); +} diff --git a/src-tauri/src/commands_public_tests.rs b/src-tauri/src/commands_public_tests.rs new file mode 100644 index 000000000..8a402432b --- /dev/null +++ b/src-tauri/src/commands_public_tests.rs @@ -0,0 +1,421 @@ +//! Deterministic coverage for command-layer pure cores without exposing them as public crate APIs. + +use crate::commands::{ + clean_dev_artifacts_inner, clean_paths_inner, execute_moves_inner, list_roots, + load_ontology_from, node_view, parse_move_entry, undo_last_moves_inner, AppState, CleanResult, + EntryView, NodeView, +}; +use crate::organize::MovePlan; +use crate::scanner::{ScanResult, ScanStats}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering; + +fn result_for(root: &Path, dir_sizes: HashMap) -> ScanResult { + ScanResult { + root: root.to_path_buf(), + dir_sizes, + top_files: Vec::new(), + stats: ScanStats::default(), + cancelled: false, + } +} + +#[test] +fn node_view_rejects_parent_outside_and_missing_paths() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("root"); + fs::create_dir(&root).unwrap(); + let result = result_for(&root, HashMap::new()); + + assert_eq!( + node_view(&result, &root.join("..")) + .err() + .expect("parent traversal must fail"), + "path outside scanned root" + ); + assert_eq!( + node_view(&result, &temp.path().join("outside")) + .err() + .expect("outside path must fail"), + "path outside scanned root" + ); + assert!(node_view(&result, &root.join("missing")).is_err()); +} + +#[test] +fn node_view_lists_files_and_directories_by_descending_size() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("root"); + let directory = root.join("directory"); + let file = root.join("file.bin"); + fs::create_dir_all(&directory).unwrap(); + fs::write(&file, [1_u8, 2, 3, 4]).unwrap(); + + #[cfg(unix)] + { + std::os::unix::fs::symlink(&file, root.join("file-link")).unwrap(); + } + + let mut sizes = HashMap::new(); + sizes.insert(root.clone(), 10); + sizes.insert(directory.clone(), 6); + let view = node_view(&result_for(&root, sizes), &root).unwrap(); + + assert_eq!(view.path, root.to_string_lossy()); + assert_eq!(view.size, 10); + assert_eq!(view.entries.len(), 2); + assert_eq!(view.entries[0].name, "directory"); + assert!(view.entries[0].is_dir); + assert_eq!(view.entries[0].size, 6); + assert_eq!(view.entries[1].name, "file.bin"); + assert!(!view.entries[1].is_dir); + assert_eq!(view.entries[1].size, 4); +} + +#[test] +fn node_view_defaults_unmeasured_directory_sizes_without_inventing_evidence() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("root"); + let unmeasured = root.join("unmeasured"); + fs::create_dir_all(&unmeasured).unwrap(); + + let view = node_view(&result_for(&root, HashMap::new()), &root).unwrap(); + + assert_eq!(view.size, 0); + assert_eq!(view.entries.len(), 1); + assert_eq!(view.entries[0].name, "unmeasured"); + assert!(view.entries[0].is_dir); + assert_eq!(view.entries[0].size, 0); +} + +#[test] +fn command_state_defaults_and_serializable_views_are_covered() { + let state = AppState::default(); + assert!(state.result.lock().unwrap().is_none()); + assert!(!state.cancel.load(Ordering::SeqCst)); + assert!(!state.scanning.load(Ordering::SeqCst)); + assert!(state.cloud_review.lock().is_ok()); + + let node = NodeView { + path: "/tmp/example".into(), + size: 7, + entries: vec![EntryView { + name: "file.bin".into(), + path: "/tmp/example/file.bin".into(), + size: 7, + is_dir: false, + }], + }; + let node_json = serde_json::to_value(&node).unwrap(); + assert_eq!(node_json["path"], "/tmp/example"); + assert_eq!(node_json["size"], 7); + assert_eq!(node_json["entries"][0]["name"], "file.bin"); + assert_eq!(node_json["entries"][0]["path"], "/tmp/example/file.bin"); + assert_eq!(node_json["entries"][0]["size"], 7); + assert_eq!(node_json["entries"][0]["is_dir"], false); + + let clean = CleanResult { + path: "/tmp/example/file.bin".into(), + ok: false, + error: "blocked".into(), + }; + let clean_json = serde_json::to_value(&clean).unwrap(); + assert_eq!(clean_json["path"], "/tmp/example/file.bin"); + assert_eq!(clean_json["ok"], false); + assert_eq!(clean_json["error"], "blocked"); +} + +#[test] +fn clean_paths_fail_closed_before_mutation_when_journaling_is_unavailable() { + let temp = tempfile::tempdir().unwrap(); + let file = temp.path().join("file.bin"); + let directory = temp.path().join("directory"); + let nested = directory.join("nested.bin"); + let missing = temp.path().join("missing.bin"); + fs::write(&file, [1_u8, 2, 3, 4]).unwrap(); + fs::create_dir(&directory).unwrap(); + fs::write(&nested, [5_u8, 6, 7]).unwrap(); + + // Passing an existing directory as the journal file makes OpenOptions fail before + // trash::delete can run. This exercises regular-file, recursive-directory, and missing-file + // accounting while proving the command core keeps every real target intact when its audit + // journal cannot be written. + let results = clean_paths_inner( + &[file.clone(), directory.clone(), missing.clone()], + temp.path(), + 99, + ); + + assert_eq!(results.len(), 3); + assert!(results.iter().all(|result| !result.ok)); + assert!(results.iter().all(|result| !result.error.is_empty())); + assert_eq!(results[0].path, file.to_string_lossy()); + assert_eq!(results[1].path, directory.to_string_lossy()); + assert_eq!(results[2].path, missing.to_string_lossy()); + assert!(file.exists()); + assert!(directory.exists()); + assert!(nested.exists()); + assert!(!missing.exists()); + + #[cfg(unix)] + { + // A filesystem root is rejected by the final safety guard without touching the journal. + let protected = clean_paths_inner(&[PathBuf::from("/")], temp.path(), 100); + assert_eq!(protected.len(), 1); + assert!(!protected[0].ok); + assert!(!protected[0].error.is_empty()); + } +} + +#[test] +fn developer_artifact_cleanup_rejects_stale_manifest_and_preserves_current_object_on_journal_failure() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("app"); + let target = project.join("target"); + fs::create_dir_all(&target).unwrap(); + fs::write(project.join("Cargo.toml"), b"[package]\nname = \"coverage-fixture\"\n").unwrap(); + fs::write(target.join("artifact.bin"), b"preserve-me").unwrap(); + + let mut requests = crate::dev_artifacts::find_artifacts(temp.path(), 0, u64::MAX); + assert_eq!(requests.len(), 1); + let current = requests.pop().unwrap(); + assert!(current.scan_complete); + assert!(!current.object_id.is_empty()); + + let mut stale = current.clone(); + stale.fingerprint.push('0'); + let stale_result = clean_dev_artifacts_inner( + &[stale], + temp.path(), + 0, + &temp.path().join("missing-journal-parent").join("operations.jsonl"), + u64::MAX, + ); + assert_eq!(stale_result.len(), 1); + assert!(!stale_result[0].ok); + assert!(stale_result[0].error.contains("다시 스캔")); + assert!(target.exists()); + + // A current manifest reaches the identity-bound recycle authority. Pointing its audit journal + // at a nonexistent parent makes journaling fail before the staged object can be renamed, so + // the fixture proves the matching/error arm without relying on the host trash provider. + let current_result = clean_dev_artifacts_inner( + &[current], + temp.path(), + 0, + &temp.path().join("missing-journal-parent").join("operations.jsonl"), + u64::MAX, + ); + assert_eq!(current_result.len(), 1); + assert!(!current_result[0].ok); + assert!(!current_result[0].error.is_empty()); + assert!(target.exists()); + assert_eq!(fs::read(target.join("artifact.bin")).unwrap(), b"preserve-me"); +} + +#[cfg(any(windows, target_os = "linux"))] +#[test] +fn developer_artifact_cleanup_recycles_current_identity_and_records_success() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("identity-success-project"); + let target = project.join("target"); + fs::create_dir_all(&target).unwrap(); + fs::write( + project.join("Cargo.toml"), + b"[package]\nname = \"identity-success-fixture\"\n", + ) + .unwrap(); + fs::write(target.join("artifact.bin"), b"recycle-me").unwrap(); + + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let mut discovered = crate::dev_artifacts::find_artifacts(temp.path(), 0, now_ms); + assert_eq!(discovered.len(), 1); + let current = discovered.pop().unwrap(); + assert!(current.scan_complete); + assert!(!current.object_id.is_empty()); + + let journal = temp.path().join("operations.jsonl"); + let result = clean_dev_artifacts_inner( + &[current], + temp.path(), + 0, + &journal, + now_ms, + ); + + assert_eq!(result.len(), 1); + assert!(result[0].ok, "{}", result[0].error); + assert!(result[0].error.is_empty()); + assert!(!target.exists(), "reviewed artifact must leave its source path"); + + let recent = crate::safety::journal_recent(&journal, 10); + assert_eq!(recent.len(), 2); + assert_eq!(recent[0].outcome, "ok"); + assert_eq!(recent[1].outcome, "pending"); + + // The identity-bound trash authority first moves the reviewed `target` into a private sibling + // staging directory. Match both that original parent and the item name so this cleanup can + // never purge unrelated user trash that happens to contain a directory named `target`. + let items: Vec<_> = trash::os_limited::list() + .unwrap() + .into_iter() + .filter(|item| item.name == std::ffi::OsStr::new("target")) + .filter(|item| item.original_parent.starts_with(&project)) + .collect(); + assert_eq!(items.len(), 1, "exact staged fixture must be present in trash"); + trash::os_limited::purge_all(items).unwrap(); +} + +#[test] +fn developer_artifact_cleanup_rejects_each_mutable_request_identity_field() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("app"); + let target = project.join("target"); + fs::create_dir_all(&target).unwrap(); + fs::write(project.join("Cargo.toml"), b"[package]\nname = \"branch-coverage-fixture\"\n").unwrap(); + fs::write(target.join("artifact.bin"), b"stable").unwrap(); + + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let mut discovered = crate::dev_artifacts::find_artifacts(temp.path(), 0, now_ms); + assert_eq!(discovered.len(), 1); + let current = discovered.pop().unwrap(); + assert!(current.scan_complete); + assert_eq!(current.skipped, 0); + assert!(!current.object_id.is_empty()); + + let mut variants = Vec::new(); + + let mut changed = current.clone(); + changed.path.push_str("-replacement"); + variants.push(changed); + + let mut changed = current.clone(); + changed.kind.push_str("-replacement"); + variants.push(changed); + + let mut changed = current.clone(); + changed.project.push_str("-replacement"); + variants.push(changed); + + let mut changed = current.clone(); + changed.bytes = changed.bytes.saturating_add(1); + variants.push(changed); + + let mut changed = current.clone(); + changed.files = changed.files.saturating_add(1); + variants.push(changed); + + let mut changed = current.clone(); + changed.skipped = 1; + variants.push(changed); + + let mut changed = current.clone(); + changed.scan_complete = false; + variants.push(changed); + + let mut changed = current.clone(); + changed.fingerprint.push('0'); + variants.push(changed); + + let mut changed = current.clone(); + changed.object_id.clear(); + variants.push(changed); + + let mut changed = current.clone(); + changed.object_id.push_str("-replacement"); + variants.push(changed); + + let mut changed = current.clone(); + changed.age_days = changed.age_days.saturating_add(1); + variants.push(changed); + + for request in variants { + let result = clean_dev_artifacts_inner( + &[request], + temp.path(), + 0, + &temp.path().join("unused-journal.jsonl"), + now_ms, + ); + assert_eq!(result.len(), 1); + assert!(!result[0].ok); + assert!(result[0].error.contains("다시 스캔")); + assert!(target.exists()); + assert_eq!(fs::read(target.join("artifact.bin")).unwrap(), b"stable"); + } +} + +#[test] +fn move_execution_journaling_and_undo_form_one_reversible_flow() { + let temp = tempfile::tempdir().unwrap(); + let source = temp.path().join("source.txt"); + let destination = temp.path().join("destination.txt"); + let journal = temp.path().join("operations.jsonl"); + fs::write(&source, b"reversible").unwrap(); + + let plan = MovePlan { + src: source.to_string_lossy().into_owned(), + dst: destination.to_string_lossy().into_owned(), + class_id: "test-class".into(), + }; + let executed = execute_moves_inner(std::slice::from_ref(&plan), &journal, 100); + assert_eq!(executed.len(), 1); + assert!(executed[0].ok, "{}", executed[0].error); + assert!(!source.exists()); + assert!(destination.exists()); + assert_eq!( + parse_move_entry(&format!("{} -> {}", source.display(), destination.display())), + Some(( + source.to_string_lossy().into_owned(), + destination.to_string_lossy().into_owned() + )) + ); + assert_eq!(parse_move_entry("missing separator"), None); + + let undone = undo_last_moves_inner(1, &journal, 101); + assert_eq!(undone.len(), 1); + assert!(undone[0].ok, "{}", undone[0].error); + assert!(source.exists()); + assert!(!destination.exists()); + assert!(undo_last_moves_inner(0, &journal, 102).is_empty()); + + let missing = MovePlan { + src: temp.path().join("missing.txt").to_string_lossy().into_owned(), + dst: temp.path().join("never-created.txt").to_string_lossy().into_owned(), + class_id: "test-class".into(), + }; + let failed = execute_moves_inner(&[missing], &journal, 103); + assert_eq!(failed.len(), 1); + assert!(!failed[0].ok); + assert!(!failed[0].error.is_empty()); +} + +#[test] +fn roots_and_ontology_wrappers_reach_their_real_pure_implementations() { + let roots = list_roots(); + #[cfg(not(windows))] + { + assert_eq!(roots.first().map(String::as_str), Some("/")); + if let Ok(home) = std::env::var("HOME") { + assert!(roots.iter().any(|root| root == &home)); + } + } + #[cfg(windows)] + { + assert!(roots + .iter() + .all(|root| root.len() == 3 && root.ends_with(":\\"))); + } + + let ontology = load_ontology_from(include_str!("../resources/ontology/default.ttl")).unwrap(); + assert!(!ontology.classes.is_empty()); + assert!(load_ontology_from("this is not Turtle").is_err()); +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ad9481876..3e20da9b6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,10 @@ compile_error!("DiskSage supports only Windows, Linux, and macOS targets."); mod dupes; #[cfg_attr(coverage, allow(dead_code))] mod commands; +#[cfg(test)] +mod commands_public_tests; +#[cfg(test)] +mod commands_env_coverage_tests; #[cfg_attr(coverage, allow(dead_code))] mod generic_cleanup; #[cfg_attr(coverage, allow(dead_code))] From 33beca938a8340948edcd1bb433cebbd6fa831b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:00:12 +0900 Subject: [PATCH 22/30] test(cloud): inherit current-compatible coverage evidence --- src-tauri/tests/cloud_public_coverage.rs | 373 +++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 src-tauri/tests/cloud_public_coverage.rs diff --git a/src-tauri/tests/cloud_public_coverage.rs b/src-tauri/tests/cloud_public_coverage.rs new file mode 100644 index 000000000..94025f391 --- /dev/null +++ b/src-tauri/tests/cloud_public_coverage.rs @@ -0,0 +1,373 @@ +//! Public-contract coverage for deterministic cloud-root validation. +//! +//! The tests use temporary local directories only. They do not discover real cloud accounts, +//! invoke provider APIs, mutate files, or require credentials. + +#![cfg(not(coverage))] + +use disksage_lib::cloud::{ + cloud_root_path_matches, discover_cloud_roots, discover_cloud_roots_report, + plan_cloud_archive_from_snapshot, prepare_cloud_archive_source, validate_cloud_root_readable, + validate_source_root_readable, CloudAccountScope, CloudPlanOptions, CloudProvider, CloudRoot, + ContentMetadata, FileFact, +}; +use unicode_normalization::UnicodeNormalization; + +fn cloud_root(path: &std::path::Path, readable: bool, access_issue: Option<&str>) -> CloudRoot { + CloudRoot { + id: "coverage-root".into(), + provider: CloudProvider::GoogleDrive, + account_scope: CloudAccountScope::Unknown, + label: "Coverage Root".into(), + path: path.to_string_lossy().into_owned(), + readable, + access_issue: access_issue.map(str::to_owned), + } +} + +#[test] +fn source_and_destination_roots_fail_closed_before_planning() { + let temp = tempfile::tempdir().unwrap(); + let directory = temp.path().join("cloud-root"); + std::fs::create_dir(&directory).unwrap(); + let regular_file = temp.path().join("not-a-directory"); + std::fs::write(®ular_file, b"content").unwrap(); + let missing = temp.path().join("missing"); + + assert!(validate_source_root_readable(&directory).is_ok()); + assert!(validate_source_root_readable(®ular_file) + .unwrap_err() + .starts_with("source-root-not-directory:")); + assert!(validate_source_root_readable(&missing) + .unwrap_err() + .starts_with("source-root-not-directory:")); + + assert!(validate_cloud_root_readable(&cloud_root(&directory, true, None)).is_ok()); + assert_eq!( + validate_cloud_root_readable(&cloud_root( + &directory, + false, + Some("permission-denied") + )) + .unwrap_err(), + format!( + "cloud-root-unreadable:{}:permission-denied", + directory.to_string_lossy() + ) + ); + assert_eq!( + validate_cloud_root_readable(&cloud_root(&directory, false, None)).unwrap_err(), + format!( + "cloud-root-unreadable:{}:not-verified", + directory.to_string_lossy() + ) + ); + assert!(validate_cloud_root_readable(&cloud_root(&missing, true, None)) + .unwrap_err() + .starts_with("cloud-root-unreadable:")); +} + +#[test] +fn cloud_root_matching_handles_exact_canonical_and_unicode_equivalent_paths() { + let temp = tempfile::tempdir().unwrap(); + let real = temp.path().join("real"); + std::fs::create_dir(&real).unwrap(); + let alias = temp.path().join("alias"); + + #[cfg(unix)] + { + std::os::unix::fs::symlink(&real, &alias).unwrap(); + assert!(cloud_root_path_matches(&real, &alias)); + } + + assert!(cloud_root_path_matches(&real, &real)); + assert!(!cloud_root_path_matches( + &real, + &temp.path().join("different") + )); + + let composed = temp.path().join("Café"); + let decomposed_text = "Café".nfd().collect::(); + let decomposed = temp.path().join(decomposed_text); + assert_ne!(composed, decomposed); + assert!(cloud_root_path_matches(&composed, &decomposed)); +} + +#[cfg(unix)] +#[test] +fn cloud_root_matching_rejects_distinct_non_utf8_fallback_paths() { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + use std::path::PathBuf; + + let discovered = PathBuf::from(OsString::from_vec(vec![b'/', b't', b'm', b'p', b'/', 0xff])); + let requested = PathBuf::from(OsString::from_vec(vec![b'/', b't', b'm', b'p', b'/', 0xfe])); + + assert_ne!(discovered, requested); + assert!(!cloud_root_path_matches(&discovered, &requested)); +} + +#[test] +fn provider_and_scope_wire_values_remain_stable() { + assert_eq!(CloudProvider::Icloud.as_str(), "icloud"); + assert_eq!(CloudProvider::Onedrive.as_str(), "onedrive"); + assert_eq!(CloudProvider::GoogleDrive.as_str(), "google-drive"); + + assert_eq!(CloudAccountScope::Personal.as_str(), "personal"); + assert_eq!(CloudAccountScope::Organization.as_str(), "organization"); + assert_eq!(CloudAccountScope::Shared.as_str(), "shared"); + assert_eq!(CloudAccountScope::Unknown.as_str(), "unknown"); +} + +#[test] +fn source_snapshot_applies_selection_policy_and_reports_totals() { + const DAY_MS: u64 = 24 * 60 * 60 * 1_000; + + let temp = tempfile::tempdir().unwrap(); + let source_root = temp.path().join("source"); + std::fs::create_dir(&source_root).unwrap(); + let now_ms = 10 * DAY_MS; + let prepared_metadata = ContentMetadata { + title: Some("coverage fixture".into()), + ..ContentMetadata::default() + }; + let file = |path: std::path::PathBuf, bytes: u64, modified_ms: u64| FileFact { + path, + bytes, + created_ms: modified_ms, + modified_ms, + content_metadata: prepared_metadata.clone(), + }; + let files = vec![ + file(source_root.join("eligible.pdf"), 20, now_ms - 3 * DAY_MS), + file(source_root.join("too-small.pdf"), 9, now_ms - 3 * DAY_MS), + file(source_root.join("too-young.pdf"), 20, now_ms - DAY_MS), + file(source_root.join("missing-date.pdf"), 20, 0), + file(source_root.join("unsupported.rs"), 20, now_ms - 3 * DAY_MS), + file(temp.path().join("outside.pdf"), 20, now_ms - 3 * DAY_MS), + ]; + let options = CloudPlanOptions { + min_size_bytes: 10, + min_age_days: 2, + limit: 7, + }; + + let snapshot = prepare_cloud_archive_source(&files, &source_root, now_ms, options); + assert_eq!(snapshot.candidate_count(), 1); + assert_eq!(snapshot.candidate_bytes(), 20); + + let defaults = CloudPlanOptions::default(); + assert_eq!(defaults.min_size_bytes, 256 * 1024 * 1024); + assert_eq!(defaults.min_age_days, 90); + assert_eq!(defaults.limit, 200); +} + +#[test] +fn source_snapshot_recognizes_supported_archive_families_without_io() { + const DAY_MS: u64 = 24 * 60 * 60 * 1_000; + + let temp = tempfile::tempdir().unwrap(); + let source_root = temp.path().join("source"); + std::fs::create_dir(&source_root).unwrap(); + let now_ms = 20 * DAY_MS; + let modified_ms = now_ms - DAY_MS; + let metadata = ContentMetadata::default(); + let file = |name: &str| FileFact { + path: source_root.join(name), + bytes: 1, + created_ms: modified_ms, + modified_ms, + content_metadata: metadata.clone(), + }; + + let files = vec![ + file("document.docx"), + file("media.mp4"), + file("archive.zip"), + file("dataset.csv"), + file("backup.bak"), + file("creative.psd"), + file("unsupported.rs"), + ]; + let snapshot = prepare_cloud_archive_source( + &files, + &source_root, + now_ms, + CloudPlanOptions { + min_size_bytes: 1, + min_age_days: 0, + limit: files.len(), + }, + ); + + assert_eq!(snapshot.candidate_count(), 6); + assert_eq!(snapshot.candidate_bytes(), 6); +} + +#[test] +fn destination_plan_enforces_the_candidate_limit_after_source_admission() { + const DAY_MS: u64 = 24 * 60 * 60 * 1_000; + + let temp = tempfile::tempdir().unwrap(); + let source_root = temp.path().join("source"); + let destination_root = temp.path().join("cloud"); + std::fs::create_dir(&source_root).unwrap(); + std::fs::create_dir(&destination_root).unwrap(); + let now_ms = 20 * DAY_MS; + let modified_ms = now_ms - DAY_MS; + let prepared_metadata = ContentMetadata { + title: Some("coverage fixture".into()), + ..ContentMetadata::default() + }; + let file = |name: &str| FileFact { + path: source_root.join(name), + bytes: 2, + created_ms: modified_ms, + modified_ms, + content_metadata: prepared_metadata.clone(), + }; + let files = vec![file("one.pdf"), file("two.pdf"), file("three.pdf")]; + + let snapshot = prepare_cloud_archive_source( + &files, + &source_root, + now_ms, + CloudPlanOptions { + min_size_bytes: 1, + min_age_days: 0, + limit: 2, + }, + ); + + assert_eq!(snapshot.candidate_count(), 3); + assert_eq!(snapshot.candidate_bytes(), 6); + + let report = plan_cloud_archive_from_snapshot( + &snapshot, + &cloud_root(&destination_root, true, None), + ); + assert_eq!(report.candidates.len(), 2); + assert_eq!(report.candidate_bytes, 4); +} + +#[test] +fn discovery_classifies_synthetic_provider_roots_without_touching_real_accounts() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + + std::fs::create_dir_all(home.join("Library/Mobile Documents/com~apple~CloudDocs")).unwrap(); + std::fs::create_dir(home.join("iCloudDrive")).unwrap(); + + let cloud_storage = home.join("Library/CloudStorage"); + std::fs::create_dir_all(cloud_storage.join("OneDrive-alice@outlook.com")).unwrap(); + let google = cloud_storage.join("GoogleDrive-user@gmail.com"); + std::fs::create_dir_all(google.join("My Drive")).unwrap(); + std::fs::create_dir(google.join("Shared Drives")).unwrap(); + std::fs::create_dir(google.join(".hidden")).unwrap(); + + std::fs::create_dir(home.join("OneDrive - Contoso")).unwrap(); + std::fs::create_dir(home.join("Google Drive")).unwrap(); + + let report = discover_cloud_roots_report(home); + assert!(report.issues.is_empty(), "{:?}", report.issues); + assert!(report.roots.iter().all(|root| root.readable)); + assert!(report.roots.iter().all(|root| root.access_issue.is_none())); + + assert!(report.roots.iter().any(|root| { + root.provider == CloudProvider::Icloud + && root.account_scope == CloudAccountScope::Unknown + && root.label == "iCloud Drive" + })); + assert!(report.roots.iter().any(|root| { + root.provider == CloudProvider::Onedrive + && root.account_scope == CloudAccountScope::Personal + && root.label.contains("alice@outlook.com") + })); + assert!(report.roots.iter().any(|root| { + root.provider == CloudProvider::Onedrive + && root.account_scope == CloudAccountScope::Organization + && root.label.contains("OneDrive - Contoso") + })); + assert!(report.roots.iter().any(|root| { + root.provider == CloudProvider::GoogleDrive + && root.account_scope == CloudAccountScope::Personal + && root.label.ends_with("My Drive") + })); + assert!(report.roots.iter().any(|root| { + root.provider == CloudProvider::GoogleDrive + && root.account_scope == CloudAccountScope::Shared + && root.label.ends_with("Shared Drives") + })); + assert!(!report.roots.iter().any(|root| root.label.contains(".hidden"))); + + let roots = discover_cloud_roots(home); + assert_eq!(roots, report.roots); +} + +#[test] +fn discovery_reports_non_directory_provider_candidates_fail_closed() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + std::fs::create_dir_all(home.join("Library/CloudStorage")).unwrap(); + std::fs::write(home.join("OneDrive"), b"not-a-directory").unwrap(); + + let report = discover_cloud_roots_report(home); + assert!(report.roots.iter().all(|root| root.path != home.join("OneDrive").to_string_lossy())); + assert!(report.issues.iter().any(|issue| { + issue.provider == Some(CloudProvider::Onedrive) && issue.reason == "not-a-directory" + })); +} + +#[cfg(unix)] +#[test] +fn discovery_rejects_read_only_provider_roots_fail_closed() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + std::fs::create_dir_all(home.join("Library/CloudStorage")).unwrap(); + let provider_root = home.join("OneDrive"); + std::fs::create_dir(&provider_root).unwrap(); + + let mut read_only = std::fs::metadata(&provider_root).unwrap().permissions(); + read_only.set_mode(0o555); + std::fs::set_permissions(&provider_root, read_only).unwrap(); + + let report = discover_cloud_roots_report(home); + + let mut restored = std::fs::metadata(&provider_root).unwrap().permissions(); + restored.set_mode(0o755); + std::fs::set_permissions(&provider_root, restored).unwrap(); + + assert!(report + .roots + .iter() + .all(|root| root.path != provider_root.to_string_lossy())); + assert!(report.issues.iter().any(|issue| { + issue.provider == Some(CloudProvider::Onedrive) + && issue.path == provider_root.to_string_lossy() + && issue.reason == "read-only" + })); +} + +#[cfg(unix)] +#[test] +fn discovery_deduplicates_provider_aliases_by_canonical_identity() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + std::fs::create_dir_all(home.join("Library/CloudStorage")).unwrap(); + let target = home.join("provider-target"); + std::fs::create_dir(&target).unwrap(); + std::os::unix::fs::symlink(&target, home.join("OneDrive")).unwrap(); + std::os::unix::fs::symlink(&target, home.join("OneDrive - Contoso")).unwrap(); + + let report = discover_cloud_roots_report(home); + let onedrive_roots = report + .roots + .iter() + .filter(|root| root.provider == CloudProvider::Onedrive) + .count(); + + assert_eq!(onedrive_roots, 1); + assert!(report.issues.is_empty(), "{:?}", report.issues); +} From 44e872f5858be459ce8c9dc62e684dd65aa6fe50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:09:04 +0900 Subject: [PATCH 23/30] docs(coverage): bind current measured gap recovery --- docs/development/coverage-evidence.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/development/coverage-evidence.md b/docs/development/coverage-evidence.md index eca6db78c..1de4777d5 100644 --- a/docs/development/coverage-evidence.md +++ b/docs/development/coverage-evidence.md @@ -74,6 +74,16 @@ The group intentionally omits source SHA so a newer first-attempt run can supers GitHub documents concurrency groups as the mechanism for limiting simultaneous workflow/job execution and canceling outdated runs. It also documents workflow artifacts as the mechanism for persisting outputs such as test and coverage results after a job completes. +## Latest measured gap and recovery + +The latest completed repository-wide Rust measurement that produced usable coverage totals is predecessor head `af39bce9bb6ac3186e3940e2c94dd8381080f619` from Test run `33779617794`. It measured 64,391/80,218 regions (80.270014%), 5,292/8,991 branches (58.858859%), 3,357/4,924 functions (68.176280%), and 42,867/53,111 lines (80.712094%). Those values are RED evidence: none is reusable as passing evidence for a later head. + +The bounded diagnostic identified `src-tauri/src/commands.rs` (2,364 uncovered lines), `src-tauri/src/cloud.rs` (687), `src-tauri/src/icloud_sync_health.rs` (474), and `src-tauri/src/provider_oauth.rs` (388) as the largest then-current uncovered production contributors. Recovery is therefore ordered by measured contribution instead of by arbitrary test-file count. + +Current coverage-owner lineage has adopted the still-valid command-layer public coverage and HOME-absent environment fixture from historical PR #156, then adopted `src-tauri/tests/cloud_public_coverage.rs` after verifying its public cloud contracts against current `cloud.rs`. These are test-only changes; they do not narrow the denominator or substitute synthetic data for destructive/recovery acceptance. Their value is not considered inherited until an unchanged exact head compiles, runs them, and emits the next real measurement. + +The next historical donor, `src-tauri/tests/icloud_sync_health_public_coverage.rs`, is not source-compatible verbatim. Current `IcloudSyncHealthReport` includes `native_status` and `file_provider_activity`; the older fixture predates those fields. Its filesystem/admission semantics remain candidates for recovery, but the donor must first be minimally adapted to the current report shape and then pass exact-head execution. Copying a stale blob merely to increase test count is not acceptable evidence. + ## Operating rule A missing `coverage-evidence` artifact is not passing. Queued, pending, canceled, failed, stale-head, malformed, less-than-100%, predecessor, or synthetic-merge evidence is non-passing. Engineers must add realistic tests or remove genuinely unreachable production code; they must not lower thresholds, hard-code percentages, narrow the production denominator, or reuse an artifact from a different head. From b478fc174a7b77a60e960053715e55d282a5c166 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:04:05 +0900 Subject: [PATCH 24/30] test(coverage): restore iCloud sync-health public evidence --- .../icloud_sync_health_public_coverage.rs | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src-tauri/tests/icloud_sync_health_public_coverage.rs diff --git a/src-tauri/tests/icloud_sync_health_public_coverage.rs b/src-tauri/tests/icloud_sync_health_public_coverage.rs new file mode 100644 index 000000000..00c34782c --- /dev/null +++ b/src-tauri/tests/icloud_sync_health_public_coverage.rs @@ -0,0 +1,222 @@ +use disksage_lib::icloud_sync_health::{ + attach_new_copy_admission_notice, default_cloud_docs_db_dir, inspect_new_copy_admission, + probe_icloud_sync_health, require_new_copy_admission, IcloudSyncHealthReport, + IcloudUploadQueueSummary, ICLOUD_SYNC_HEALTH_SCHEMA_VERSION, +}; +use std::fs; +use std::path::Path; + +fn admission_report() -> IcloudSyncHealthReport { + IcloudSyncHealthReport { + schema_version: ICLOUD_SYNC_HEALTH_SCHEMA_VERSION, + output_mode: "icloud-local-sync-health".into(), + observed_at_ms: 1, + provider: "icloud".into(), + evidence_kind: "supplementary-local-cloud-docs-private-schema".into(), + evidence_complete: true, + database_snapshot_includes_wal: false, + database_sidecar_write_permitted: false, + managed_database_files: Vec::new(), + managed_database_allocated_bytes: 0, + upload_queue: IcloudUploadQueueSummary::default(), + native_status: None, + file_provider_activity: None, + sync_backlog_present: false, + new_copy_admission_state: "clear".into(), + new_copy_admission_blockers: Vec::new(), + blockers: Vec::new(), + notices: Vec::new(), + paths_redacted: true, + user_filenames_read: false, + user_file_contents_read: false, + remote_capacity_verified: false, + provider_sync_attested: false, + local_eviction_authorized: false, + mutation_performed: false, + } +} + +fn sorted_entry_names(directory: &Path) -> Vec { + let mut names = fs::read_dir(directory) + .unwrap() + .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) + .collect::>(); + names.sort(); + names +} + +#[test] +fn public_probe_rejects_unsafe_database_directory_shapes_before_sqlite() { + assert_eq!( + probe_icloud_sync_health(Path::new("relative/cloud-docs-db"), 1).unwrap_err(), + "icloud-sync-health-db-dir-not-absolute" + ); + + let temp = tempfile::tempdir().unwrap(); + let missing = temp.path().join("missing"); + assert_eq!( + probe_icloud_sync_health(&missing, 1).unwrap_err(), + "icloud-sync-health-db-dir-unavailable" + ); + + let regular_file = temp.path().join("not-a-directory"); + fs::write(®ular_file, b"not a database directory").unwrap(); + assert_eq!( + probe_icloud_sync_health(®ular_file, 1).unwrap_err(), + "icloud-sync-health-db-dir-unsafe" + ); + + #[cfg(unix)] + { + let real_directory = temp.path().join("real-db-directory"); + fs::create_dir(&real_directory).unwrap(); + let linked_directory = temp.path().join("linked-db-directory"); + std::os::unix::fs::symlink(&real_directory, &linked_directory).unwrap(); + assert_eq!( + probe_icloud_sync_health(&linked_directory, 1).unwrap_err(), + "icloud-sync-health-db-dir-unsafe" + ); + } +} + +#[test] +fn public_probe_rejects_missing_or_non_regular_managed_database_files() { + let temp = tempfile::tempdir().unwrap(); + let db_dir = temp.path().join("db"); + fs::create_dir(&db_dir).unwrap(); + + assert_eq!( + probe_icloud_sync_health(&db_dir, 1).unwrap_err(), + "icloud-sync-health-client.db-unavailable" + ); + + let client_db = db_dir.join("client.db"); + fs::create_dir(&client_db).unwrap(); + assert_eq!( + probe_icloud_sync_health(&db_dir, 1).unwrap_err(), + "icloud-sync-health-client.db-not-regular-file" + ); + fs::remove_dir(&client_db).unwrap(); + fs::write(&client_db, b"sqlite fixture placeholder").unwrap(); + + let original_client_db = fs::read(&client_db).unwrap(); + let source_entries = sorted_entry_names(&db_dir); + let error = probe_icloud_sync_health(&db_dir, 1).unwrap_err(); + assert!(error.starts_with("icloud-sync-health-"), "{error}"); + assert_eq!(fs::read(&client_db).unwrap(), original_client_db); + assert_eq!(sorted_entry_names(&db_dir), source_entries); + + #[cfg(unix)] + { + let real_client_db = temp.path().join("real-client.db"); + fs::write(&real_client_db, b"sqlite fixture target").unwrap(); + fs::remove_file(&client_db).unwrap(); + std::os::unix::fs::symlink(&real_client_db, &client_db).unwrap(); + assert_eq!( + probe_icloud_sync_health(&db_dir, 1).unwrap_err(), + "icloud-sync-health-client.db-symlink-rejected" + ); + fs::remove_file(&client_db).unwrap(); + fs::write(&client_db, b"sqlite fixture placeholder").unwrap(); + } + + let optional_sidecar = db_dir.join("client.db-shm"); + fs::create_dir(&optional_sidecar).unwrap(); + assert_eq!( + probe_icloud_sync_health(&db_dir, 1).unwrap_err(), + "icloud-sync-health-client.db-shm-not-regular-file" + ); + + #[cfg(unix)] + { + fs::remove_dir(&optional_sidecar).unwrap(); + std::os::unix::fs::symlink(&client_db, &optional_sidecar).unwrap(); + assert_eq!( + probe_icloud_sync_health(&db_dir, 1).unwrap_err(), + "icloud-sync-health-client.db-shm-symlink-rejected" + ); + } +} + +#[test] +fn public_admission_gate_rejects_inconsistent_or_explicitly_blocked_reports() { + let clear = admission_report(); + assert!(require_new_copy_admission(&clear).is_ok()); + + let mut inconsistent = admission_report(); + inconsistent.new_copy_admission_state = "blocked".into(); + assert_eq!( + require_new_copy_admission(&inconsistent).unwrap_err(), + "icloud-new-copy-admission-invalid" + ); + + let mut blocked = admission_report(); + blocked.new_copy_admission_state = "blocked".into(); + blocked.new_copy_admission_blockers = vec![ + "icloud-upload-out-of-quota".into(), + "icloud-upload-in-flight".into(), + ]; + assert_eq!( + require_new_copy_admission(&blocked).unwrap_err(), + "icloud-upload-out-of-quota,icloud-upload-in-flight" + ); + + let mut incomplete = admission_report(); + incomplete.evidence_complete = false; + assert_eq!( + require_new_copy_admission(&incomplete).unwrap_err(), + "icloud-sync-health-evidence-incomplete" + ); +} + +#[test] +fn public_copy_admission_helpers_replace_only_owned_notices() { + let clear = admission_report(); + let mut blocked = admission_report(); + blocked.new_copy_admission_state = "blocked".into(); + blocked.new_copy_admission_blockers = vec!["icloud-upload-in-flight".into()]; + + let mut notices = vec![ + "unrelated-evidence".into(), + "icloud-new-copy-admission-blocked".into(), + "icloud-new-copy-admission-evidence-unavailable".into(), + ]; + attach_new_copy_admission_notice(&mut notices, Some(&clear)); + assert_eq!( + notices, + vec!["unrelated-evidence", "icloud-new-copy-admission-clear"] + ); + + attach_new_copy_admission_notice(&mut notices, Some(&blocked)); + assert_eq!( + notices, + vec!["unrelated-evidence", "icloud-new-copy-admission-blocked"] + ); + + attach_new_copy_admission_notice(&mut notices, None); + assert_eq!( + notices, + vec![ + "unrelated-evidence", + "icloud-new-copy-admission-evidence-unavailable" + ] + ); +} + +#[test] +fn public_default_database_path_and_home_inspection_preserve_absolute_path_authority() { + let home = Path::new("/Users/disk-sage-coverage"); + assert_eq!( + default_cloud_docs_db_dir(home), + home.join("Library") + .join("Application Support") + .join("CloudDocs") + .join("session") + .join("db") + ); + + assert_eq!( + inspect_new_copy_admission(Path::new("relative-home"), 123).unwrap_err(), + "icloud-sync-health-db-dir-not-absolute" + ); +} From b46ed290040f7fc081888c03c0332e156c68ce61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:04:48 +0900 Subject: [PATCH 25/30] docs(coverage): record iCloud donor adaptation --- docs/development/coverage-evidence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/coverage-evidence.md b/docs/development/coverage-evidence.md index 1de4777d5..7bdc32d7c 100644 --- a/docs/development/coverage-evidence.md +++ b/docs/development/coverage-evidence.md @@ -82,7 +82,7 @@ The bounded diagnostic identified `src-tauri/src/commands.rs` (2,364 uncovered l Current coverage-owner lineage has adopted the still-valid command-layer public coverage and HOME-absent environment fixture from historical PR #156, then adopted `src-tauri/tests/cloud_public_coverage.rs` after verifying its public cloud contracts against current `cloud.rs`. These are test-only changes; they do not narrow the denominator or substitute synthetic data for destructive/recovery acceptance. Their value is not considered inherited until an unchanged exact head compiles, runs them, and emits the next real measurement. -The next historical donor, `src-tauri/tests/icloud_sync_health_public_coverage.rs`, is not source-compatible verbatim. Current `IcloudSyncHealthReport` includes `native_status` and `file_provider_activity`; the older fixture predates those fields. Its filesystem/admission semantics remain candidates for recovery, but the donor must first be minimally adapted to the current report shape and then pass exact-head execution. Copying a stale blob merely to increase test count is not acceptable evidence. +Historical `src-tauri/tests/icloud_sync_health_public_coverage.rs` remained semantically valid but was not source-compatible verbatim because current `IcloudSyncHealthReport` added the optional `native_status` and `file_provider_activity` evidence fields. Commit `b478fc174a7b77a60e960053715e55d282a5c166` restores the donor on the current coverage owner with only those two fields bound to `None`; the filesystem/admission assertions and source-database non-mutation checks are otherwise unchanged. This is adopted pending unchanged-head compile/runtime evidence and the next real repository-wide measurement; the source-shape repair is not itself passing coverage evidence. ## Operating rule From 222343048b8a8d99e47275e12643032666726645 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:35:52 +0900 Subject: [PATCH 26/30] test: restore real Git worktree safety coverage --- .../tests/git_worktree_public_coverage.rs | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 src-tauri/tests/git_worktree_public_coverage.rs diff --git a/src-tauri/tests/git_worktree_public_coverage.rs b/src-tauri/tests/git_worktree_public_coverage.rs new file mode 100644 index 000000000..1e8b1d95a --- /dev/null +++ b/src-tauri/tests/git_worktree_public_coverage.rs @@ -0,0 +1,313 @@ +//! Public-contract coverage for the read-only Git worktree audit boundary. +//! +//! The fixtures create local temporary repositories only. They perform no fetch, branch deletion, +//! provider operation, or user-file cleanup. The removal-flow regression mutates only a temporary +//! Git worktree created by the test and verifies that its branch is retained. + +use disksage_lib::git_worktree::{ + approve_stale_worktree_removal, audit_git_worktrees, execute_stale_worktree_removal, + public_summary, GitWorktreeAuditOptions, GitWorktreeDisposition, GIT_WORKTREE_AUDIT_SCHEMA_KIND, +}; +use std::path::Path; +use std::process::Command; + +fn git(cwd: &Path, args: &[&str]) { + let status = Command::new("git") + .current_dir(cwd) + .args(args) + .status() + .expect("git must be available in the test environment"); + assert!(status.success(), "git {args:?} failed"); +} + +fn initialized_repository() -> tempfile::TempDir { + let temp = tempfile::tempdir().unwrap(); + git(temp.path(), &["init", "-q"]); + git(temp.path(), &["config", "user.name", "DiskSage Test"]); + git(temp.path(), &["config", "user.email", "disksage@example.invalid"]); + std::fs::write(temp.path().join("tracked.txt"), b"tracked\n").unwrap(); + git(temp.path(), &["add", "tracked.txt"]); + git(temp.path(), &["commit", "-q", "-m", "initial"]); + temp +} + +#[test] +fn option_and_reference_admission_fail_closed_before_audit() { + let root = initialized_repository(); + let defaults = GitWorktreeAuditOptions::default(); + + let invalid_options = [ + GitWorktreeAuditOptions { command_timeout_ms: 0, ..defaults }, + GitWorktreeAuditOptions { command_timeout_ms: 300_001, ..defaults }, + GitWorktreeAuditOptions { size_scan_timeout_ms: 0, ..defaults }, + GitWorktreeAuditOptions { size_scan_timeout_ms: 600_001, ..defaults }, + GitWorktreeAuditOptions { max_worktrees: 0, ..defaults }, + GitWorktreeAuditOptions { max_worktrees: 10_001, ..defaults }, + GitWorktreeAuditOptions { max_entries_per_worktree: 0, ..defaults }, + GitWorktreeAuditOptions { max_entries_per_worktree: 20_000_001, ..defaults }, + GitWorktreeAuditOptions { max_active_pids: 0, ..defaults }, + GitWorktreeAuditOptions { max_active_pids: 4_097, ..defaults }, + ]; + for options in invalid_options { + assert!(audit_git_worktrees(root.path(), &["HEAD".into()], options, 1).is_err()); + } + + assert_eq!( + audit_git_worktrees( + Path::new("relative"), + &["HEAD".into()], + defaults, + 1, + ) + .unwrap_err(), + "git-worktree-repository-root-not-absolute" + ); + assert!(audit_git_worktrees(root.path(), &[], defaults, 1).is_err()); + for reference in ["", "-dangerous", "bad\nref"] { + assert_eq!( + audit_git_worktrees(root.path(), &[reference.into()], defaults, 1).unwrap_err(), + "git-worktree-reference-invalid" + ); + } + assert_eq!( + audit_git_worktrees(root.path(), &["x".repeat(1_025)], defaults, 1).unwrap_err(), + "git-worktree-reference-invalid" + ); +} + +#[test] +fn retained_primary_worktree_is_preserved_with_privacy_safe_public_summary() { + let root = initialized_repository(); + let report = audit_git_worktrees( + root.path(), + &["HEAD".into(), "HEAD".into()], + GitWorktreeAuditOptions::default(), + 123, + ) + .unwrap(); + + assert_eq!(report.schema_kind, GIT_WORKTREE_AUDIT_SCHEMA_KIND); + assert_eq!(report.version, 2); + assert_eq!(report.generated_at_ms, 123); + assert_eq!(report.retention_references.len(), 1); + assert_eq!(report.retention_reference_set_fingerprint.len(), 64); + assert!(report.retention_reachable_commit_count >= 1); + assert_eq!(report.worktree_count, 1); + assert_eq!(report.removal_candidate_count, 0); + assert_eq!(report.preserved_count, 1); + assert_eq!(report.evidence_gap_count, 0); + assert!(report.evidence_complete); + assert_eq!(report.exact_approval_phrase, None); + assert!(!report.filesystem_mutation_executed); + + let entry = &report.entries[0]; + assert!(entry.primary); + assert!(entry.audit_origin); + assert!(entry.head_is_retained_tip); + assert_eq!(entry.status_clean, Some(true)); + assert_eq!(entry.status_entry_count, Some(0)); + assert_eq!(entry.contained_in_reference, Some(true)); + assert_eq!(entry.disposition, GitWorktreeDisposition::Preserve); + for blocker in ["primary-worktree", "audit-origin-worktree", "head-is-retained-tip"] { + assert!(entry.blockers.contains(&blocker.to_string())); + } + assert!(entry.size.evidence_complete); + assert!(!entry.active_use.assessed); + assert_eq!( + entry.active_use.error.as_deref(), + Some("active-use-not-needed-for-preserved-worktree") + ); + assert_eq!(entry.path_fingerprint.len(), 64); + assert_eq!(entry.entry_fingerprint.len(), 64); + + let summary = public_summary(&report); + assert_eq!(summary.schema_kind, GIT_WORKTREE_AUDIT_SCHEMA_KIND); + assert_eq!(summary.worktree_count, 1); + assert!(summary.local_paths_redacted); + assert!(summary.branch_names_redacted); + assert!(summary + .metadata_semantics + .contains(&"user-file-production-time-not-inferred".to_string())); + assert!(summary.notices.contains(&"read-only-audit".to_string())); + let serialized = serde_json::to_string(&summary).unwrap(); + assert!(!serialized.contains(root.path().to_string_lossy().as_ref())); + assert!(!serialized.contains("refs/heads/")); + + assert_eq!( + approve_stale_worktree_removal( + &report, + "not-authorized", + 124, + "human:test", + "Reviewed retained primary worktree", + ) + .unwrap_err(), + "git-worktree-removal-audit-not-executable" + ); +} + +#[test] +fn removal_approval_rejects_tampered_audit_evidence_before_authorization() { + let root = initialized_repository(); + let report = audit_git_worktrees( + root.path(), + &["HEAD".into()], + GitWorktreeAuditOptions::default(), + 700, + ) + .unwrap(); + + let reject = |candidate: &disksage_lib::git_worktree::GitWorktreeAuditReport| { + approve_stale_worktree_removal( + candidate, + "not-authorized", + 701, + "human:test", + "Review tamper-resistant worktree evidence", + ) + .unwrap_err() + }; + + let mut invalid_envelope = report.clone(); + invalid_envelope.schema_kind = "disksage.git-worktree-audit/forged".into(); + assert_eq!( + reject(&invalid_envelope), + "git-worktree-removal-audit-integrity-invalid" + ); + + let mut invalid_reference_binding = report.clone(); + invalid_reference_binding.retention_reference_set_fingerprint = "0".repeat(64); + assert_eq!( + reject(&invalid_reference_binding), + "git-worktree-removal-reference-binding-mismatch" + ); + + let mut invalid_entry = report.clone(); + invalid_entry.entries[0].path_fingerprint = "0".repeat(64); + assert_eq!( + reject(&invalid_entry), + "git-worktree-removal-entry-integrity-mismatch" + ); + + let mut invalid_summary = report; + invalid_summary.worktree_count = invalid_summary.worktree_count.saturating_add(1); + assert_eq!( + reject(&invalid_summary), + "git-worktree-removal-audit-summary-mismatch" + ); +} + +#[test] +fn dirty_primary_state_is_observed_without_becoming_removal_authority() { + let root = initialized_repository(); + std::fs::write(root.path().join("untracked.txt"), b"local-only\n").unwrap(); + + let report = audit_git_worktrees( + root.path(), + &["HEAD".into()], + GitWorktreeAuditOptions::default(), + 456, + ) + .unwrap(); + let entry = &report.entries[0]; + assert_eq!(entry.status_clean, Some(false)); + assert_eq!(entry.status_entry_count, Some(1)); + assert!(entry.blockers.contains(&"worktree-dirty".to_string())); + assert_eq!(entry.disposition, GitWorktreeDisposition::Preserve); + assert_eq!(report.removal_candidate_count, 0); + assert!(!report.filesystem_mutation_executed); +} + +#[cfg(unix)] +#[test] +fn clean_merged_secondary_worktree_requires_exact_approval_then_removes_only_the_worktree() { + let root = initialized_repository(); + git(root.path(), &["branch", "stale-test-worktree"]); + + std::fs::write(root.path().join("newer.txt"), b"retained tip\n").unwrap(); + git(root.path(), &["add", "newer.txt"]); + git(root.path(), &["commit", "-q", "-m", "advance retained tip"]); + + let secondary_parent = tempfile::tempdir().unwrap(); + let secondary = secondary_parent.path().join("stale-worktree"); + let secondary_text = secondary.to_string_lossy().into_owned(); + git( + root.path(), + &["worktree", "add", "-q", &secondary_text, "stale-test-worktree"], + ); + + let options = GitWorktreeAuditOptions::default(); + let report = audit_git_worktrees(root.path(), &["HEAD".into()], options, 1_000).unwrap(); + assert_eq!(report.worktree_count, 2); + assert_eq!(report.removal_candidate_count, 1); + assert_eq!(report.preserved_count, 1); + assert_eq!(report.evidence_gap_count, 0); + assert!(report.evidence_complete); + + let candidate = report + .entries + .iter() + .find(|entry| entry.path == secondary_text) + .expect("secondary worktree must be in the exact audit"); + assert_eq!(candidate.disposition, GitWorktreeDisposition::RemovalCandidate); + assert!(candidate.blockers.is_empty()); + assert_eq!(candidate.status_clean, Some(true)); + assert_eq!(candidate.contained_in_reference, Some(true)); + assert!(!candidate.head_is_retained_tip); + assert_eq!(candidate.actor_cwd_inside, Some(false)); + assert!(candidate.size.evidence_complete); + assert!(candidate.active_use.assessed); + assert!(candidate.active_use.evidence_complete); + assert!(!candidate.active_use.active); + + let phrase = report + .exact_approval_phrase + .clone() + .expect("one verified candidate must produce an exact approval phrase"); + assert_eq!( + approve_stale_worktree_removal( + &report, + "wrong confirmation", + 1_001, + "human:test", + "Reviewed clean merged temporary worktree", + ) + .unwrap_err(), + "git-worktree-removal-exact-approval-required" + ); + let approval = approve_stale_worktree_removal( + &report, + &phrase, + 1_001, + "human:test", + "Reviewed clean merged temporary worktree", + ) + .unwrap(); + assert_eq!(approval.removal_candidate_count, 1); + assert_eq!(approval.exact_approval_phrase, phrase); + + let result = execute_stale_worktree_removal(&report, &approval, &phrase, options, 1_002).unwrap(); + assert!(result.verification_complete); + assert_eq!(result.planned_candidate_count, 1); + assert_eq!(result.attempted_count, 1); + assert_eq!(result.removed_count, 1); + assert!(result.filesystem_mutation_executed); + assert!(!result.branch_delete_executed); + assert!(!result.git_prune_executed); + assert_eq!(result.stopped_reason, None); + assert_eq!(result.items.len(), 1); + let item = &result.items[0]; + assert!(item.removal_attempted); + assert!(item.removal_command_succeeded); + assert!(item.path_absence_verified); + assert!(item.registration_absence_verified); + assert_eq!(item.branch_retained, Some(true)); + assert_eq!(item.error, None); + assert!(!secondary.exists()); + assert!(root.path().exists()); + + git( + root.path(), + &["show-ref", "--verify", "--quiet", "refs/heads/stale-test-worktree"], + ); +} From 8d3ce22f862c7c9669d3e0aba9d732e487b138ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:36:41 +0900 Subject: [PATCH 27/30] docs: record real worktree coverage adoption --- docs/development/coverage-evidence.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/development/coverage-evidence.md b/docs/development/coverage-evidence.md index 7bdc32d7c..d1e1e477e 100644 --- a/docs/development/coverage-evidence.md +++ b/docs/development/coverage-evidence.md @@ -78,12 +78,14 @@ GitHub documents concurrency groups as the mechanism for limiting simultaneous w The latest completed repository-wide Rust measurement that produced usable coverage totals is predecessor head `af39bce9bb6ac3186e3940e2c94dd8381080f619` from Test run `33779617794`. It measured 64,391/80,218 regions (80.270014%), 5,292/8,991 branches (58.858859%), 3,357/4,924 functions (68.176280%), and 42,867/53,111 lines (80.712094%). Those values are RED evidence: none is reusable as passing evidence for a later head. -The bounded diagnostic identified `src-tauri/src/commands.rs` (2,364 uncovered lines), `src-tauri/src/cloud.rs` (687), `src-tauri/src/icloud_sync_health.rs` (474), and `src-tauri/src/provider_oauth.rs` (388) as the largest then-current uncovered production contributors. Recovery is therefore ordered by measured contribution instead of by arbitrary test-file count. +The bounded diagnostic identified `src-tauri/src/commands.rs` (2,364 uncovered lines), `src-tauri/src/cloud.rs` (687), `src-tauri/src/icloud_sync_health.rs` (474), and `src-tauri/src/provider_oauth.rs` (388) as the largest then-current uncovered production contributors. The same diagnostic also recorded `provider_api_write.rs` (385), `podman_reclaim.rs` (330), `zotero_local.rs` (299), and `git_worktree.rs` (286). Recovery is ordered by measured contribution and valid owner evidence rather than arbitrary test-file count. Current coverage-owner lineage has adopted the still-valid command-layer public coverage and HOME-absent environment fixture from historical PR #156, then adopted `src-tauri/tests/cloud_public_coverage.rs` after verifying its public cloud contracts against current `cloud.rs`. These are test-only changes; they do not narrow the denominator or substitute synthetic data for destructive/recovery acceptance. Their value is not considered inherited until an unchanged exact head compiles, runs them, and emits the next real measurement. Historical `src-tauri/tests/icloud_sync_health_public_coverage.rs` remained semantically valid but was not source-compatible verbatim because current `IcloudSyncHealthReport` added the optional `native_status` and `file_provider_activity` evidence fields. Commit `b478fc174a7b77a60e960053715e55d282a5c166` restores the donor on the current coverage owner with only those two fields bound to `None`; the filesystem/admission assertions and source-database non-mutation checks are otherwise unchanged. This is adopted pending unchanged-head compile/runtime evidence and the next real repository-wide measurement; the source-shape repair is not itself passing coverage evidence. +`provider_oauth.rs` remains owned by provider-OAuth hardening PR #339, so the coverage branch does not copy its domain tests. Among the next inspected historical donors, `podman_reclaim_public_coverage.rs` relies on synthetic Podman JSON and a fake executable for its broad success path, so it is not used as destructive/recovery acceptance evidence here. Commit `222343048b8a8d99e47275e12643032666726645` instead restores `git_worktree_public_coverage.rs`, whose fixtures create real temporary Git repositories and whose mutation case creates and removes only a real temporary secondary worktree after exact approval, while verifying the branch is retained. That adoption targets the measured 286-line `git_worktree.rs` gap without altering production code or weakening the coverage denominator. It remains pending unchanged-head hosted compile/runtime evidence and a new real measurement. + ## Operating rule A missing `coverage-evidence` artifact is not passing. Queued, pending, canceled, failed, stale-head, malformed, less-than-100%, predecessor, or synthetic-merge evidence is non-passing. Engineers must add realistic tests or remove genuinely unreachable production code; they must not lower thresholds, hard-code percentages, narrow the production denominator, or reuse an artifact from a different head. From 969f790ef92d2b6c2705281f2d468008ac63c93d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:13:53 +0900 Subject: [PATCH 28/30] test(ci): align coverage workflow with canonical supersession contract --- src/lib/testWorkflowConcurrencyContract.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/testWorkflowConcurrencyContract.test.ts b/src/lib/testWorkflowConcurrencyContract.test.ts index 71a8a8dfe..cffa87267 100644 --- a/src/lib/testWorkflowConcurrencyContract.test.ts +++ b/src/lib/testWorkflowConcurrencyContract.test.ts @@ -13,7 +13,7 @@ function readTestWorkflow(): string { } describe('Test workflow supersession', () => { - it('cancels obsolete first-attempt work for one ref without making reruns self-cancel', () => { + it('cancels only obsolete first-attempt pull-request work without making reruns self-cancel', () => { const workflow = readTestWorkflow(); const concurrencyStart = workflow.indexOf('concurrency:'); const permissionsStart = workflow.indexOf('permissions:'); @@ -23,10 +23,10 @@ describe('Test workflow supersession', () => { const concurrencyBlock = workflow.slice(concurrencyStart, permissionsStart); expect(concurrencyBlock).toContain( - 'group: test-${{ github.workflow }}-${{ github.ref }}', + 'group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}', ); expect(concurrencyBlock).toContain( - 'cancel-in-progress: ${{ github.run_attempt == 1 }}', + "cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}", ); expect(concurrencyBlock).not.toContain('github.sha'); expect(concurrencyBlock).not.toContain('pull_request.head.sha'); From 3813044e9c3d6811a592945b050a99e5bafae792 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 05:33:40 +0900 Subject: [PATCH 29/30] test(coverage): preserve dirty secondary worktrees --- .../git_worktree_dirty_secondary_coverage.rs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src-tauri/tests/git_worktree_dirty_secondary_coverage.rs diff --git a/src-tauri/tests/git_worktree_dirty_secondary_coverage.rs b/src-tauri/tests/git_worktree_dirty_secondary_coverage.rs new file mode 100644 index 000000000..b7d27ae15 --- /dev/null +++ b/src-tauri/tests/git_worktree_dirty_secondary_coverage.rs @@ -0,0 +1,105 @@ +//! Real-repository coverage for a dirty secondary Git worktree. +//! +//! A secondary worktree whose HEAD is already contained in the retained tip would otherwise be a +//! reclaim candidate. Local untracked content must make that exact worktree non-executable while +//! preserving both the worktree and its branch. + +use disksage_lib::git_worktree::{ + audit_git_worktrees, GitWorktreeAuditOptions, GitWorktreeDisposition, +}; +use std::path::Path; +use std::process::Command; + +fn git(cwd: &Path, args: &[&str]) { + let status = Command::new("git") + .current_dir(cwd) + .args(args) + .status() + .expect("git must be available in the test environment"); + assert!(status.success(), "git {args:?} failed"); +} + +fn initialized_repository() -> tempfile::TempDir { + let temp = tempfile::tempdir().expect("create temporary Git repository"); + git(temp.path(), &["init", "-q"]); + git(temp.path(), &["config", "user.name", "DiskSage Test"]); + git( + temp.path(), + &["config", "user.email", "disksage@example.invalid"], + ); + std::fs::write(temp.path().join("tracked.txt"), b"tracked\n") + .expect("write initial tracked fixture"); + git(temp.path(), &["add", "tracked.txt"]); + git(temp.path(), &["commit", "-q", "-m", "initial"]); + temp +} + +#[cfg(unix)] +#[test] +fn dirty_secondary_worktree_is_preserved_even_when_its_head_is_retained() { + let root = initialized_repository(); + git(root.path(), &["branch", "dirty-stale-worktree"]); + + std::fs::write(root.path().join("retained.txt"), b"new retained tip\n") + .expect("advance retained branch fixture"); + git(root.path(), &["add", "retained.txt"]); + git(root.path(), &["commit", "-q", "-m", "advance retained tip"]); + + let secondary_parent = tempfile::tempdir().expect("create secondary worktree parent"); + let secondary = secondary_parent.path().join("dirty-worktree"); + let secondary_text = secondary.to_string_lossy().into_owned(); + git( + root.path(), + &[ + "worktree", + "add", + "-q", + &secondary_text, + "dirty-stale-worktree", + ], + ); + std::fs::write(secondary.join("local-only.txt"), b"must be preserved\n") + .expect("create local untracked worktree content"); + + let report = audit_git_worktrees( + root.path(), + &["HEAD".into()], + GitWorktreeAuditOptions::default(), + 2_000, + ) + .expect("audit local Git worktrees"); + + assert_eq!(report.worktree_count, 2); + assert_eq!(report.removal_candidate_count, 0); + assert_eq!(report.exact_approval_phrase, None); + assert!(!report.filesystem_mutation_executed); + + let entry = report + .entries + .iter() + .find(|entry| entry.path == secondary_text) + .expect("secondary worktree must be represented in exact evidence"); + assert!(!entry.primary); + assert!(!entry.audit_origin); + assert!(entry.contained_in_reference == Some(true)); + assert!(!entry.head_is_retained_tip); + assert_eq!(entry.status_clean, Some(false)); + assert!(entry.status_entry_count.is_some_and(|count| count >= 1)); + assert!(entry.blockers.contains(&"worktree-dirty".to_string())); + assert_eq!(entry.disposition, GitWorktreeDisposition::Preserve); + + assert!(secondary.exists()); + assert_eq!( + std::fs::read(secondary.join("local-only.txt")).expect("local content must remain"), + b"must be preserved\n" + ); + git( + root.path(), + &[ + "show-ref", + "--verify", + "--quiet", + "refs/heads/dirty-stale-worktree", + ], + ); +} From 1394402fae3f5c04ed0d94994d114245d4cc0f4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 07:12:16 +0900 Subject: [PATCH 30/30] test: preserve prunable worktrees as evidence gaps --- .../tests/git_worktree_prunable_coverage.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src-tauri/tests/git_worktree_prunable_coverage.rs diff --git a/src-tauri/tests/git_worktree_prunable_coverage.rs b/src-tauri/tests/git_worktree_prunable_coverage.rs new file mode 100644 index 000000000..f9df5c033 --- /dev/null +++ b/src-tauri/tests/git_worktree_prunable_coverage.rs @@ -0,0 +1,98 @@ +//! Coverage for stale Git worktree registrations whose filesystem path disappeared. +//! +//! The fixture deletes only a temporary test worktree directory. The production audit must retain +//! the stale registration as an evidence gap instead of treating Git's `prunable` metadata as +//! removal authority. + +use disksage_lib::git_worktree::{ + audit_git_worktrees, GitWorktreeAuditOptions, GitWorktreeDisposition, +}; +use std::path::Path; +use std::process::Command; + +fn git(cwd: &Path, args: &[&str]) { + let status = Command::new("git") + .current_dir(cwd) + .args(args) + .status() + .expect("git must be available in the test environment"); + assert!(status.success(), "git {args:?} failed"); +} + +fn initialized_repository() -> tempfile::TempDir { + let temp = tempfile::tempdir().unwrap(); + git(temp.path(), &["init", "-q"]); + git(temp.path(), &["config", "user.name", "DiskSage Test"]); + git(temp.path(), &["config", "user.email", "disksage@example.invalid"]); + std::fs::write(temp.path().join("tracked.txt"), b"tracked\n").unwrap(); + git(temp.path(), &["add", "tracked.txt"]); + git(temp.path(), &["commit", "-q", "-m", "initial"]); + temp +} + +#[test] +fn missing_secondary_path_is_prunable_but_remains_an_evidence_gap() { + let root = initialized_repository(); + git(root.path(), &["branch", "prunable-test-worktree"]); + + std::fs::write(root.path().join("retained.txt"), b"retained\n").unwrap(); + git(root.path(), &["add", "retained.txt"]); + git(root.path(), &["commit", "-q", "-m", "advance retained tip"]); + + let secondary_parent = tempfile::tempdir().unwrap(); + let secondary = secondary_parent.path().join("missing-worktree"); + let secondary_text = secondary.to_string_lossy().into_owned(); + git( + root.path(), + &["worktree", "add", "-q", &secondary_text, "prunable-test-worktree"], + ); + std::fs::remove_dir_all(&secondary).unwrap(); + + let report = audit_git_worktrees( + root.path(), + &["HEAD".into()], + GitWorktreeAuditOptions::default(), + 4_000, + ) + .unwrap(); + let entry = report + .entries + .iter() + .find(|entry| entry.path == secondary_text) + .expect("stale registered worktree must remain visible to the audit"); + + assert!(entry.prunable); + assert!(entry.prunable_reason.is_some()); + assert_eq!(entry.status_clean, None); + assert!(!entry.size.evidence_complete); + assert_eq!( + entry.size.error.as_deref(), + Some("worktree-path-evidence-incomplete") + ); + assert_eq!(entry.disposition, GitWorktreeDisposition::EvidenceGap); + assert!(entry + .blockers + .contains(&"worktree-prunable-metadata".to_string())); + assert!(entry + .blockers + .contains(&"worktree-path-evidence-incomplete".to_string())); + assert!(entry + .blockers + .contains(&"git-status-evidence-incomplete".to_string())); + match entry.actor_cwd_inside { + Some(false) => assert!(!entry + .blockers + .contains(&"actor-cwd-evidence-incomplete".to_string())), + None => assert!(entry + .blockers + .contains(&"actor-cwd-evidence-incomplete".to_string())), + Some(true) => panic!("unrelated stale worktree must not contain the actor CWD"), + } + assert!(entry + .blockers + .contains(&"size-evidence-incomplete".to_string())); + assert!(report.evidence_gap_count >= 1); + assert!(!report.evidence_complete); + assert!(!report.issues.is_empty()); + assert!(!report.filesystem_mutation_executed); +}