From 7016c95fa5850339577fa1447d4694892ff96992 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:53:59 +0900 Subject: [PATCH 01/30] test: require bounded scan and navigation errors --- src/routes/pageErrorFeedbackContract.test.ts | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/routes/pageErrorFeedbackContract.test.ts diff --git a/src/routes/pageErrorFeedbackContract.test.ts b/src/routes/pageErrorFeedbackContract.test.ts new file mode 100644 index 000000000..ef95f5321 --- /dev/null +++ b/src/routes/pageErrorFeedbackContract.test.ts @@ -0,0 +1,36 @@ +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 readSource(path: string): string { + return readFileSync(resolve(repositoryRoot, path), "utf8"); +} + +describe("main scan and navigation failure feedback", () => { + it("does not expose arbitrary exceptions or hide operation failures in the console", () => { + const source = readSource("src/routes/+page.svelte"); + + expect(source).not.toContain('alert(`스캔 시작 실패: ${e}`)'); + expect(source).not.toContain('console.error("post-scan load failed:", e)'); + expect(source).not.toContain('console.error("getNode failed:", e)'); + expect(source).toContain("디스크 루트 목록을 불러오지 못했습니다."); + expect(source).toContain("스캔 결과를 불러오지 못했습니다."); + expect(source).toContain("스캔을 시작하지 못했습니다."); + expect(source).toContain("폴더 내용을 불러오지 못했습니다."); + }); + + it("preserves scan and navigation authority behind one accessible alert", () => { + const source = readSource("src/routes/+page.svelte"); + + expect(source).toContain('role="alert"'); + expect(source).toContain("api.listRoots()"); + expect(source).toContain("api.onScanProgress("); + expect(source).toContain("api.onScanDone("); + expect(source).toContain("api.startScan(selectedRoot)"); + expect(source).toContain("api.getNode("); + expect(source).toContain("api.topFiles(200)"); + }); +}); From 682a694ae3299924b371315f63c20f5a2b626a48 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:06:16 +0900 Subject: [PATCH 02/30] fix: bound scan and navigation failure feedback --- src/routes/+page.svelte | 72 +++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4a4254473..7a7181cc0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,58 +17,83 @@ let node: api.NodeView | null = $state(null); let crumbs: string[] = $state([]); let top: api.EntryView[] = $state([]); + let operationError = $state(""); let navSeq = 0; onMount(async () => { - roots = await api.listRoots(); - selectedRoot = roots[0] ?? ""; - await api.onScanProgress((s) => (stats = s)); - await api.onScanDone(async (s) => { - stats = s; - scanning = false; - try { - crumbs = [selectedRoot]; - node = await api.getNode(selectedRoot); - top = await api.topFiles(200); - } catch (e) { - console.error("post-scan load failed:", e); - } - }); + try { + roots = await api.listRoots(); + selectedRoot = roots[0] ?? ""; + } catch { + operationError = "디스크 루트 목록을 불러오지 못했습니다."; + } + + try { + await api.onScanProgress((s) => (stats = s)); + await api.onScanDone(async (s) => { + stats = s; + scanning = false; + operationError = ""; + try { + const scannedRoot = selectedRoot; + const [nextNode, nextTop] = await Promise.all([ + api.getNode(scannedRoot), + api.topFiles(200), + ]); + crumbs = [scannedRoot]; + node = nextNode; + top = nextTop; + } catch { + node = null; + top = []; + operationError = "스캔 결과를 불러오지 못했습니다."; + } + }); + } catch { + operationError = "스캔 이벤트 연결을 준비하지 못했습니다."; + } }); async function scan() { + operationError = ""; scanning = true; node = null; top = []; try { await api.startScan(selectedRoot); - } catch (e) { + } catch { scanning = false; - alert(`스캔 시작 실패: ${e}`); + operationError = "스캔을 시작하지 못했습니다."; } } async function open(path: string) { const seq = ++navSeq; + operationError = ""; try { const n = await api.getNode(path); if (seq !== navSeq) return; // 더 새로운 내비게이션이 이미 시작됨 crumbs = [...crumbs, path]; node = n; - } catch (e) { - console.error("getNode failed:", e); + } catch { + if (seq === navSeq) { + operationError = "폴더 내용을 불러오지 못했습니다."; + } } } async function jump(i: number) { const seq = ++navSeq; + operationError = ""; try { const n = await api.getNode(crumbs[i]); if (seq !== navSeq) return; crumbs = crumbs.slice(0, i + 1); node = n; - } catch (e) { - console.error("getNode failed:", e); + } catch { + if (seq === navSeq) { + operationError = "폴더 내용을 불러오지 못했습니다."; + } } } @@ -92,6 +117,10 @@ {/if} + {#if operationError} + + {/if} + {#if node} -
    - {#each node.entries as e} -
  • - {#if e.is_dir} - - {:else} - 📄 {e.name} - {/if} - {fmtBytes(e.size)} -
  • - {/each} -
+
+ {#if node.entries.length === 0} +

표시할 항목이 없습니다. 상위 폴더로 이동하거나 다른 폴더를 스캔하세요.

+ {:else} +
    + {#each node.entries as e} +
  • + {#if e.is_dir} + + {:else} + 📄 {e.name} + {/if} + {fmtBytes(e.size)} +
  • + {/each} +
+ {/if} +
{/if} {#if top.length > 0} @@ -175,8 +181,11 @@ .error { margin: 0.75rem 0; font-weight: 600; } .crumbs { margin: 0.75rem 0; display: flex; gap: 0.25rem; flex-wrap: wrap; } .crumb { background: none; border: none; color: #06c; cursor: pointer; padding: 0; } - .entries { list-style: none; padding: 0; max-height: 40vh; overflow-y: auto; } + .entry-scroll { max-height: 40vh; overflow-y: auto; } + .entry-scroll:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } + .entries { list-style: none; padding: 0; margin: 0; } .entries li { display: flex; justify-content: space-between; padding: 2px 0; } .dir { background: none; border: none; cursor: pointer; font: inherit; padding: 0; } .size { color: #666; font-variant-numeric: tabular-nums; } + .empty-entries { margin: 0; color: #555; } From 42f0c97ae222bb06ec2aa48f5f43f237054da6b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:42:54 +0900 Subject: [PATCH 11/30] fix(ui): make scan failures actionable and accessible --- src/routes/+page.svelte | 16 +++++++++------- .../pageEntryAccessibilityContract.test.ts | 6 +++++- src/routes/pageErrorFeedbackContract.test.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0c8d03e9d..9c631dabb 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -26,7 +26,7 @@ selectedRoot = roots[0] ?? ""; } catch { console.error("disk root load failed"); - operationError = "디스크 루트 목록을 불러오지 못했습니다."; + operationError = "디스크 목록을 불러오지 못했습니다. DiskSage를 다시 열어 주세요."; } try { @@ -51,12 +51,12 @@ node = null; top = []; console.error("post-scan result load failed"); - operationError = "스캔 결과를 불러오지 못했습니다."; + operationError = "스캔 결과를 불러오지 못했습니다. 같은 폴더를 다시 스캔하세요."; } }); } catch { console.error("scan event registration failed"); - operationError = "스캔 이벤트 연결을 준비하지 못했습니다."; + operationError = "스캔을 준비하지 못했습니다. DiskSage를 다시 열어 주세요."; } }); @@ -71,7 +71,7 @@ } catch { scanning = false; console.error("scan start failed"); - operationError = "스캔을 시작하지 못했습니다."; + operationError = "스캔을 시작하지 못했습니다. 폴더를 다시 선택한 뒤 재시도하세요."; } } @@ -86,7 +86,7 @@ } catch { if (seq === navSeq) { console.error("folder navigation failed"); - operationError = "폴더 내용을 불러오지 못했습니다."; + operationError = "폴더 내용을 불러오지 못했습니다. 상위 폴더로 돌아가 다시 여세요."; } } } @@ -102,7 +102,7 @@ } catch { if (seq === navSeq) { console.error("folder navigation failed"); - operationError = "폴더 내용을 불러오지 못했습니다."; + operationError = "폴더 내용을 불러오지 못했습니다. 상위 폴더로 돌아가 다시 여세요."; } } } @@ -139,7 +139,8 @@ {/each} -
+ 폴더 항목 탐색 시작 +
{#if node.entries.length === 0}

표시할 항목이 없습니다. 상위 폴더로 이동하거나 다른 폴더를 스캔하세요.

{:else} @@ -183,6 +184,7 @@ .crumb { background: none; border: none; color: #06c; cursor: pointer; padding: 0; } .entry-scroll { max-height: 40vh; overflow-y: auto; } .entry-scroll:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } + .entry-focus { display: inline-block; margin-block-end: 0.35rem; } .entries { list-style: none; padding: 0; margin: 0; } .entries li { display: flex; justify-content: space-between; padding: 2px 0; } .dir { background: none; border: none; cursor: pointer; font: inherit; padding: 0; } diff --git a/src/routes/pageEntryAccessibilityContract.test.ts b/src/routes/pageEntryAccessibilityContract.test.ts index a7ab21290..00c35e8d2 100644 --- a/src/routes/pageEntryAccessibilityContract.test.ts +++ b/src/routes/pageEntryAccessibilityContract.test.ts @@ -14,13 +14,17 @@ describe("canonical scan-entry accessibility surface", () => { const page = readSource("src/routes/+page.svelte"); expect(page).toContain( - '
', + '폴더 항목 탐색 시작', + ); + expect(page).toContain( + '
', ); expect(page).toContain('
    '); expect(page).toContain("{#each node.entries as e}"); expect(page).toContain('onclick={() => open(e.path)}'); expect(page).toContain("{fmtBytes(e.size)}"); expect(page).toContain(".entry-scroll:focus-visible"); + expect(page).toContain(".entry-focus"); }); it("gives an empty scan result a visible next action instead of a blank list", () => { diff --git a/src/routes/pageErrorFeedbackContract.test.ts b/src/routes/pageErrorFeedbackContract.test.ts index 745c87218..342fe82c4 100644 --- a/src/routes/pageErrorFeedbackContract.test.ts +++ b/src/routes/pageErrorFeedbackContract.test.ts @@ -36,12 +36,12 @@ describe("main scan and navigation failure feedback", () => { expect(openScope).toContain('console.error("folder navigation failed");'); expect(jumpScope).toContain('console.error("folder navigation failed");'); - expect(mountScope).toContain("디스크 루트 목록을 불러오지 못했습니다."); - expect(mountScope).toContain("스캔 결과를 불러오지 못했습니다."); - expect(mountScope).toContain("스캔 이벤트 연결을 준비하지 못했습니다."); - expect(scanScope).toContain("스캔을 시작하지 못했습니다."); - expect(openScope).toContain("폴더 내용을 불러오지 못했습니다."); - expect(jumpScope).toContain("폴더 내용을 불러오지 못했습니다."); + expect(mountScope).toContain("디스크 목록을 불러오지 못했습니다. DiskSage를 다시 열어 주세요."); + expect(mountScope).toContain("스캔 결과를 불러오지 못했습니다. 같은 폴더를 다시 스캔하세요."); + expect(mountScope).toContain("스캔을 준비하지 못했습니다. DiskSage를 다시 열어 주세요."); + expect(scanScope).toContain("스캔을 시작하지 못했습니다. 폴더를 다시 선택한 뒤 재시도하세요."); + expect(openScope).toContain("폴더 내용을 불러오지 못했습니다. 상위 폴더로 돌아가 다시 여세요."); + expect(jumpScope).toContain("폴더 내용을 불러오지 못했습니다. 상위 폴더로 돌아가 다시 여세요."); }); it("clears stale feedback and invalidates navigation before issuing new requests", () => { @@ -68,4 +68,4 @@ describe("main scan and navigation failure feedback", () => { expect(source).toContain("api.getNode("); expect(source).toContain("api.topFiles(200)"); }); -}); \ No newline at end of file +}); From 5e2e54be1a7f8e1f47680ac1ff1a607dc6b99f9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 20:40:29 +0900 Subject: [PATCH 12/30] test: reject invalid negative paths-ignore filters --- src/lib/testWorkflowPathFilterContract.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/lib/testWorkflowPathFilterContract.test.ts diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts new file mode 100644 index 000000000..b47adf89f --- /dev/null +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -0,0 +1,17 @@ +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)), "../.."); +const workflow = readFileSync(resolve(repositoryRoot, ".github/workflows/test.yml"), "utf8"); + +describe("test workflow path-filter contract", () => { + it("does not put negative globs under paths-ignore", () => { + const ignoreBlocks = workflow.matchAll(/paths-ignore:\n((?:\s+-\s+[^\n]+\n?)+)/g); + + for (const match of ignoreBlocks) { + expect(match[1]).not.toMatch(/^\s*-\s+["']?!/m); + } + }); +}); From 47d16e078f1171f64952e0a1527c69203cbd5b76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 20:40:56 +0900 Subject: [PATCH 13/30] fix(ci): use valid ordered path filters for contract docs --- .github/workflows/test.yml | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b35c94808..49f62e82c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,29 +3,31 @@ name: Test on: push: branches: [main] - 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" - - "!docs/doctoring/model-load-handle-binding.md" - - "!docs/development/icloud-local-eviction-batch.md" - - "!docs/architecture/goals/cloud-offload-goal.json" - - "!CHANGELOG.md" + paths: + - "**" + - "!docs/**" + - "!*.md" + # GitHub supports re-inclusion only with ordered positive patterns under `paths`. + - "docs/doctoring/release-artifact-provenance.md" + - "docs/doctoring/tauri-content-security-policy.md" + - "docs/doctoring/model-artifact-integrity.md" + - "docs/doctoring/model-load-handle-binding.md" + - "docs/development/icloud-local-eviction-batch.md" + - "docs/architecture/goals/cloud-offload-goal.json" + - "CHANGELOG.md" pull_request: - 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" - - "!docs/doctoring/model-load-handle-binding.md" - - "!docs/development/icloud-local-eviction-batch.md" - - "!docs/architecture/goals/cloud-offload-goal.json" - - "!CHANGELOG.md" + paths: + - "**" + - "!docs/**" + - "!*.md" + # GitHub supports re-inclusion only with ordered positive patterns under `paths`. + - "docs/doctoring/release-artifact-provenance.md" + - "docs/doctoring/tauri-content-security-policy.md" + - "docs/doctoring/model-artifact-integrity.md" + - "docs/doctoring/model-load-handle-binding.md" + - "docs/development/icloud-local-eviction-batch.md" + - "docs/architecture/goals/cloud-offload-goal.json" + - "CHANGELOG.md" permissions: contents: read From 439431af45d3b9c10fd78d798767e9dbfca6fba0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 20:51:58 +0900 Subject: [PATCH 14/30] test: reproduce paths-ignore parser blind spots --- .../testWorkflowPathFilterContract.test.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index b47adf89f..3454f32cb 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -6,12 +6,31 @@ import { describe, expect, it } from "vitest"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const workflow = readFileSync(resolve(repositoryRoot, ".github/workflows/test.yml"), "utf8"); +function negativePathsIgnoreEntries(source: string): string[] { + const entries: string[] = []; + const ignoreBlocks = source.matchAll(/paths-ignore:\n((?:\s+-\s+[^\n]+\n?)+)/g); + for (const match of ignoreBlocks) { + for (const line of match[1].split("\n")) { + const item = line.match(/^\s*-\s+["']?(![^"'\s]+)["']?\s*$/); + if (item) entries.push(item[1]); + } + } + return entries; +} + describe("test workflow path-filter contract", () => { - it("does not put negative globs under paths-ignore", () => { - const ignoreBlocks = workflow.matchAll(/paths-ignore:\n((?:\s+-\s+[^\n]+\n?)+)/g); + it("detects negative paths-ignore entries after comments and in inline lists", () => { + const fixtures = [ + `pull_request:\n paths-ignore:\n - "docs/**"\n # contract exception\n - "!docs/example.md"\n`, + `push:\n paths-ignore: ["docs/**", "!docs/example.md"]\n`, + ]; - for (const match of ignoreBlocks) { - expect(match[1]).not.toMatch(/^\s*-\s+["']?!/m); + for (const fixture of fixtures) { + expect(negativePathsIgnoreEntries(fixture)).toContain("!docs/example.md"); } }); + + it("does not put negative globs under paths-ignore", () => { + expect(negativePathsIgnoreEntries(workflow)).toEqual([]); + }); }); From 80c8971f5eca6dcdf5590491a708162a2d3a3d7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 20:52:23 +0900 Subject: [PATCH 15/30] fix(test): inspect every paths-ignore list item --- .../testWorkflowPathFilterContract.test.ts | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index 3454f32cb..c86981d0c 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -6,16 +6,56 @@ import { describe, expect, it } from "vitest"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const workflow = readFileSync(resolve(repositoryRoot, ".github/workflows/test.yml"), "utf8"); +function scalarValue(raw: string): string { + const value = raw.trim(); + if (value.startsWith('"')) { + const end = value.indexOf('"', 1); + return end >= 0 ? value.slice(1, end) : value.slice(1); + } + if (value.startsWith("'")) { + const end = value.indexOf("'", 1); + return end >= 0 ? value.slice(1, end) : value.slice(1); + } + return value.split(/\s+#/, 1)[0].trim(); +} + function negativePathsIgnoreEntries(source: string): string[] { - const entries: string[] = []; - const ignoreBlocks = source.matchAll(/paths-ignore:\n((?:\s+-\s+[^\n]+\n?)+)/g); - for (const match of ignoreBlocks) { - for (const line of match[1].split("\n")) { - const item = line.match(/^\s*-\s+["']?(![^"'\s]+)["']?\s*$/); - if (item) entries.push(item[1]); + const negatives: string[] = []; + const lines = source.split(/\r?\n/); + + for (let index = 0; index < lines.length; index += 1) { + const key = lines[index].match(/^(\s*)paths-ignore:\s*(.*)$/); + if (!key) continue; + + const keyIndent = key[1].length; + const inline = key[2].trim(); + if (inline) { + const listBody = inline.startsWith("[") && inline.endsWith("]") + ? inline.slice(1, -1) + : inline; + for (const rawItem of listBody.split(",")) { + const value = scalarValue(rawItem); + if (value.startsWith("!")) negatives.push(value); + } + continue; + } + + for (let cursor = index + 1; cursor < lines.length; cursor += 1) { + const line = lines[cursor]; + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const indent = line.length - line.trimStart().length; + if (indent <= keyIndent) break; + + const listItem = trimmed.match(/^-\s*(.+)$/); + if (!listItem) continue; + const value = scalarValue(listItem[1]); + if (value.startsWith("!")) negatives.push(value); } } - return entries; + + return negatives; } describe("test workflow path-filter contract", () => { From 190a9ee144b5cb7da9d02acb271e48782acad078 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 05:05:30 +0900 Subject: [PATCH 16/30] test(ui): prove empty TopFiles result is reachable --- .../topFilesEmptyReachabilityContract.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/routes/topFilesEmptyReachabilityContract.test.ts diff --git a/src/routes/topFilesEmptyReachabilityContract.test.ts b/src/routes/topFilesEmptyReachabilityContract.test.ts new file mode 100644 index 000000000..f2424ca95 --- /dev/null +++ b/src/routes/topFilesEmptyReachabilityContract.test.ts @@ -0,0 +1,31 @@ +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 readSource(path: string): string { + return readFileSync(resolve(repositoryRoot, path), "utf8"); +} + +describe("TopFiles completed-scan reachability", () => { + it("mounts TopFiles after a successful result load even when the result list is empty", () => { + const source = readSource("src/routes/+page.svelte"); + + expect(source).toContain("node = nextNode;\n top = nextTop;"); + expect(source).toContain("{#if node}\n \n {/if}"); + expect(source).not.toContain("{#if top.length > 0}"); + }); + + it("keeps the TopFiles surface hidden before and during a new scan", () => { + const source = readSource("src/routes/+page.svelte"); + const scanStart = source.indexOf("async function scan()"); + const openStart = source.indexOf("async function open("); + expect(scanStart).toBeGreaterThanOrEqual(0); + expect(openStart).toBeGreaterThan(scanStart); + const scanScope = source.slice(scanStart, openStart); + + expect(scanScope).toMatch(/scanning = true;[\s\S]*node = null;[\s\S]*top = \[\];/); + }); +}); From 502c4288fa4e4e69e2917b27981dc65f33ce08f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 05:06:09 +0900 Subject: [PATCH 17/30] fix(ui): render empty TopFiles guidance after completed scans --- src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9c631dabb..f22b02fab 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -160,7 +160,7 @@
{/if} - {#if top.length > 0} + {#if node} {/if} From fe2059645790f3d0a2163cbebb16fcf1763089b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:09:29 +0900 Subject: [PATCH 18/30] test(ci): require canonical Windows agent-state regression --- src/lib/testWorkflowPathFilterContract.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index c86981d0c..1acef24f4 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -73,4 +73,12 @@ describe("test workflow path-filter contract", () => { it("does not put negative globs under paths-ignore", () => { expect(negativePathsIgnoreEntries(workflow)).toEqual([]); }); + + it("runs the Windows agent-state regression when that owner source is present", () => { + expect(workflow).toContain("Test-Path 'src-tauri/src/agent_state_guard.rs'"); + expect(workflow).toContain( + "rustc --edition=2021 --test src-tauri/src/agent_state_guard.rs -o target/agent-state-guard.exe", + ); + expect(workflow).toContain("& .\\target\\agent-state-guard.exe --nocapture"); + }); }); From 29aaf64c9fea7ffde88fc9a8adcd6f0560c26466 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 23:11:24 +0900 Subject: [PATCH 19/30] fix(ci): own Windows agent-state regression in Test workflow --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba638a3b8..e56371a6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,6 +87,15 @@ jobs: New-Item -ItemType Directory -Force target | Out-Null rustc --edition=2021 --test src-tauri/tests/home_resolution_contract.rs -o target/home-resolution-contract.exe & .\target\home-resolution-contract.exe + - name: Windows agent-state regression when owner source is present + shell: pwsh + run: | + if (Test-Path 'src-tauri/src/agent_state_guard.rs') { + rustc --edition=2021 --test src-tauri/src/agent_state_guard.rs -o target/agent-state-guard.exe + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & .\target\agent-state-guard.exe --nocapture + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } llm-engine-build: runs-on: ubuntu-latest From f339ee4ad852425b65f6c059b1750238c54db5a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:00:25 +0900 Subject: [PATCH 20/30] fix(ci): run source-present macOS cache owner regressions --- .github/workflows/test.yml | 25 +++++++++++++ .../testWorkflowPathFilterContract.test.ts | 35 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e56371a6e..d726ff183 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,6 +73,31 @@ jobs: - run: npm test - run: npm run build + macos-cache-cleanup: + runs-on: macos-latest + timeout-minutes: 20 + steps: + - 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 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + workspaces: src-tauri + cache-targets: false + - name: macOS cache cleanup regressions when owner source is present + env: + TMPDIR: ${{ runner.temp }} + run: | + for test_name in cache_cleanup_corepack_scope cache_cleanup_cli_permanent_gradle generated_cache_staged_activity; do + if [[ -f "src-tauri/tests/${test_name}.rs" ]]; then + cargo test --manifest-path src-tauri/Cargo.toml --test "$test_name" + else + printf 'SKIP %s: owner test source absent; no runtime regression executed\n' "$test_name" + fi + done + windows-home-resolution: runs-on: windows-latest timeout-minutes: 10 diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index 1acef24f4..4bb946cee 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -1,4 +1,6 @@ -import { readFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -82,3 +84,34 @@ describe("test workflow path-filter contract", () => { expect(workflow).toContain("& .\\target\\agent-state-guard.exe --nocapture"); }); }); + +// Exercise the canonical shell admission without compiling or faking Rust test results. +it("macOS cache job executes present owner tests, reports absent source, and propagates failure", () => { + const job = workflow.split(" macos-cache-cleanup:\n")[1]?.split(" windows-home-resolution:")[0] ?? ""; + expect(job).toContain("runs-on: macos-latest"); + expect(job).toContain("ref: ${{ github.event.pull_request.head.sha || github.sha }}"); + const script = job.match(/ run: \|\n([\s\S]*)/)?.[1].replace(/^ /gm, "") ?? ""; + for (const target of ["cache_cleanup_corepack_scope", "cache_cleanup_cli_permanent_gradle", "generated_cache_staged_activity"]) { + expect(script).toContain(target); + } + const fixture = mkdtempSync(resolve(tmpdir(), "disksage-workflow-admission-")); + try { + const bin = resolve(fixture, "bin"); + mkdirSync(bin); + const log = resolve(fixture, "cargo.log"); + writeFileSync(resolve(bin, "cargo"), '#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> "$CARGO_LOG"\nexit "${CARGO_EXIT:-0}"\n', { mode: 0o700 }); + const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, CARGO_LOG: log }; + const run = (extra = {}) => spawnSync("bash", ["-e", "-c", script], { cwd: fixture, env: { ...env, ...extra }, encoding: "utf8" }); + const absent = run(); + expect(absent.status).toBe(0); + expect(absent.stdout.match(/no runtime regression executed/g)).toHaveLength(3); + expect(existsSync(log)).toBe(false); + mkdirSync(resolve(fixture, "src-tauri/tests"), { recursive: true }); + writeFileSync(resolve(fixture, "src-tauri/tests/generated_cache_staged_activity.rs"), ""); + expect(run().status).toBe(0); + expect(readFileSync(log, "utf8")).toBe("test --manifest-path src-tauri/Cargo.toml --test generated_cache_staged_activity\n"); + expect(run({ CARGO_EXIT: "7" }).status).toBe(7); + } finally { + rmSync(fixture, { recursive: true, force: true }); + } +}); From dad7832cbc20acf8b709b6ed28e06e3db6319b12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:02:18 +0900 Subject: [PATCH 21/30] test: quote workflow command fixture correctly --- src/lib/testWorkflowPathFilterContract.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index 4bb946cee..c145cc45b 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -99,7 +99,7 @@ it("macOS cache job executes present owner tests, reports absent source, and pro const bin = resolve(fixture, "bin"); mkdirSync(bin); const log = resolve(fixture, "cargo.log"); - writeFileSync(resolve(bin, "cargo"), '#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> "$CARGO_LOG"\nexit "${CARGO_EXIT:-0}"\n', { mode: 0o700 }); + writeFileSync(resolve(bin, "cargo"), "#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$CARGO_LOG\"\nexit \"${CARGO_EXIT:-0}\"\n", { mode: 0o700 }); const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, CARGO_LOG: log }; const run = (extra = {}) => spawnSync("bash", ["-e", "-c", script], { cwd: fixture, env: { ...env, ...extra }, encoding: "utf8" }); const absent = run(); From 13aa16724fb7921c68ae5399138fa4693c823130 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:12:34 +0900 Subject: [PATCH 22/30] test(ci): require provider OAuth Windows process contract --- src/lib/testWorkflowPathFilterContract.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index c145cc45b..ee899930b 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -83,6 +83,13 @@ describe("test workflow path-filter contract", () => { ); expect(workflow).toContain("& .\\target\\agent-state-guard.exe --nocapture"); }); + + it("runs the provider OAuth Windows process contract when that owner source is present", () => { + expect(workflow).toContain("Test-Path 'src-tauri/tests/provider_oauth_cli_process.rs'"); + expect(workflow).toContain( + "cargo test --manifest-path src-tauri/Cargo.toml --locked --features cloud-cli --test provider_oauth_cli_process", + ); + }); }); // Exercise the canonical shell admission without compiling or faking Rust test results. From 8b0e2b529bff0640cef87e2aa7b6c15f40c28655 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:13:09 +0900 Subject: [PATCH 23/30] fix(ci): run provider OAuth process contract on Windows --- .github/workflows/test.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c306df497..e8fda855c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,7 +101,7 @@ jobs: windows-home-resolution: runs-on: windows-latest - timeout-minutes: 10 + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -123,6 +123,15 @@ jobs: & .\target\agent-state-guard.exe --nocapture if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } + - name: Windows provider OAuth process contract when owner source is present + shell: pwsh + run: | + if (Test-Path 'src-tauri/tests/provider_oauth_cli_process.rs') { + cargo test --manifest-path src-tauri/Cargo.toml --locked --features cloud-cli --test provider_oauth_cli_process + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Output 'SKIP provider_oauth_cli_process: owner test source absent; no runtime regression executed' + } llm-engine-build: runs-on: ubuntu-latest From 0e53be704338bb458a85372430d99f760ca17506 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:33:45 +0900 Subject: [PATCH 24/30] test(ci): require explicit agent-state skip evidence --- src/lib/testWorkflowPathFilterContract.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index ee899930b..fc3bfbde5 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -84,6 +84,12 @@ describe("test workflow path-filter contract", () => { expect(workflow).toContain("& .\\target\\agent-state-guard.exe --nocapture"); }); + it("reports absent Windows agent-state source without claiming runtime evidence", () => { + expect(workflow).toContain( + "SKIP agent_state_guard: owner source absent; no runtime regression executed", + ); + }); + it("runs the provider OAuth Windows process contract when that owner source is present", () => { expect(workflow).toContain("Test-Path 'src-tauri/tests/provider_oauth_cli_process.rs'"); expect(workflow).toContain( From e17c2ad0363651559109e4954c0af9c747fb24a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:34:10 +0900 Subject: [PATCH 25/30] fix(ci): make agent-state source absence explicit --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8fda855c..0d1e9bf08 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,6 +122,8 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & .\target\agent-state-guard.exe --nocapture if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Output 'SKIP agent_state_guard: owner source absent; no runtime regression executed' } - name: Windows provider OAuth process contract when owner source is present shell: pwsh @@ -130,7 +132,7 @@ jobs: cargo test --manifest-path src-tauri/Cargo.toml --locked --features cloud-cli --test provider_oauth_cli_process if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } else { - Write-Output 'SKIP provider_oauth_cli_process: owner test source absent; no runtime regression executed' + Write-Output 'SKIP provider_oauth_cli_process: owner source absent; no runtime regression executed' } llm-engine-build: From 8a10edc2613431ffd199ccdf2b9751defeafcdcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 23:35:21 +0900 Subject: [PATCH 26/30] test(ci): make exact-head checkout contract lane-extensible --- src/lib/testWorkflowExactHeadContract.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/testWorkflowExactHeadContract.test.ts b/src/lib/testWorkflowExactHeadContract.test.ts index 008abb15e..2acdc1e5f 100644 --- a/src/lib/testWorkflowExactHeadContract.test.ts +++ b/src/lib/testWorkflowExactHeadContract.test.ts @@ -13,7 +13,7 @@ describe("Test workflow checkout provenance", () => { line.includes("- uses: actions/checkout@") ? [index] : [], ); - expect(checkoutIndexes).toHaveLength(3); + expect(checkoutIndexes.length).toBeGreaterThanOrEqual(4); for (const checkoutIndex of checkoutIndexes) { const stepIndent = lines[checkoutIndex].match(/^(\s*)/)?.[1] ?? ""; let endIndex = checkoutIndex + 1; From f6c1d9a79fe83b6ef4dfdb3f6bf3b441bda0e2d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 02:43:45 +0900 Subject: [PATCH 27/30] fix(ci): isolate Ubuntu dependency refresh --- .github/workflows/test.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da9561fc4..506691ff5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,12 @@ jobs: persist-credentials: false - name: Install Tauri system deps run: | - sudo apt-get update + for source_file in /etc/apt/sources.list.d/*; do + if [[ -f "$source_file" ]] && grep -q 'dl.google.com/linux/chrome' "$source_file"; then + sudo rm -f "$source_file" + fi + done + sudo apt-get -o Acquire::Retries=3 update 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 @@ -153,7 +158,12 @@ jobs: persist-credentials: false - name: Install build deps (llama.cpp native + tauri) run: | - sudo apt-get update + for source_file in /etc/apt/sources.list.d/*; do + if [[ -f "$source_file" ]] && grep -q 'dl.google.com/linux/chrome' "$source_file"; then + sudo rm -f "$source_file" + fi + done + sudo apt-get -o Acquire::Retries=3 update sudo apt-get install -y cmake clang libclang-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 @@ -161,4 +171,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 --manifest-path src-tauri/Cargo.toml --features llm-engine --lib --no-run \ No newline at end of file From f5a74024dc152bc13effc6eb68e200afd583d66d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 02:44:21 +0900 Subject: [PATCH 28/30] test(ci): lock Ubuntu apt isolation contract --- src/lib/testWorkflowPathFilterContract.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/testWorkflowPathFilterContract.test.ts b/src/lib/testWorkflowPathFilterContract.test.ts index fc3bfbde5..202856d6e 100644 --- a/src/lib/testWorkflowPathFilterContract.test.ts +++ b/src/lib/testWorkflowPathFilterContract.test.ts @@ -96,6 +96,13 @@ describe("test workflow path-filter contract", () => { "cargo test --manifest-path src-tauri/Cargo.toml --locked --features cloud-cli --test provider_oauth_cli_process", ); }); + + it("isolates Ubuntu dependency refresh from the hosted runner Chrome repository without weakening apt verification", () => { + expect(workflow.match(/grep -q 'dl\.google\.com\/linux\/chrome'/g)).toHaveLength(2); + expect(workflow.match(/apt-get -o Acquire::Retries=3 update/g)).toHaveLength(2); + expect(workflow).not.toContain("AllowInsecureRepositories"); + expect(workflow).not.toContain("--allow-unauthenticated"); + }); }); // Exercise the canonical shell admission without compiling or faking Rust test results. @@ -127,4 +134,4 @@ it("macOS cache job executes present owner tests, reports absent source, and pro } finally { rmSync(fixture, { recursive: true, force: true }); } -}); +}); \ No newline at end of file From dcb538580668e10ed2d30d9c6a6612468952bf21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 11:48:19 +0900 Subject: [PATCH 29/30] test(ui): require scan to fail closed without a root --- src/routes/pageErrorFeedbackContract.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/routes/pageErrorFeedbackContract.test.ts b/src/routes/pageErrorFeedbackContract.test.ts index 342fe82c4..4853126fd 100644 --- a/src/routes/pageErrorFeedbackContract.test.ts +++ b/src/routes/pageErrorFeedbackContract.test.ts @@ -68,4 +68,11 @@ describe("main scan and navigation failure feedback", () => { expect(source).toContain("api.getNode("); expect(source).toContain("api.topFiles(200)"); }); + + it("does not leave the scan action enabled when no root is available", () => { + const source = readSource("src/routes/+page.svelte"); + const controls = between(source, '
', "{#if stats}"); + + expect(controls).toContain("disabled={scanning || !selectedRoot}"); + }); }); From f48a57dbcaea41dc06899bd230b306fa1b84e1bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 11:50:35 +0900 Subject: [PATCH 30/30] fix(ui): fail closed scan control without a root --- src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7487b4546..4cf729951 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -118,7 +118,7 @@ {#if scanning} {:else} - + {/if} {#if stats}