From f2ad04022f9e11145c57c1c8d4bdbf0b5c5def2a Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 13:22:13 +0000 Subject: [PATCH 01/28] chore(ci): use cacheable extension test lanes --- .github/workflows/code-qa.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index cc5835e081..170d1336cc 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -150,12 +150,22 @@ jobs: restore-keys: | ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}- ${{ runner.os }}-turbo- - - name: Run non-core coverage - run: pnpm turbo run test:coverage --filter="!@roo-code/core" --log-order grouped --output-logs new-only + - name: Run non-extension package coverage + run: pnpm turbo run test:coverage --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only + - name: Run extension unit coverage + run: pnpm turbo run test:coverage:unit --filter="zoo-code" --log-order grouped --output-logs new-only + - name: Run extension dist smoke test + run: pnpm turbo run test:dist --filter="zoo-code" --log-order grouped --output-logs new-only - name: Run core unit coverage run: pnpm turbo run test:coverage:unit --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Run core integration coverage run: pnpm turbo run test:coverage:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only + - name: Verify extension unit coverage report + shell: bash + run: | + test -s src/coverage/unit/lcov.info + grep -q '^SF:' src/coverage/unit/lcov.info + grep -Eq '^LF:[1-9][0-9]*$' src/coverage/unit/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -177,7 +187,7 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: >- - src/coverage/lcov.info, + src/coverage/unit/lcov.info, packages/cloud/coverage/lcov.info, packages/telemetry/coverage/lcov.info, apps/cli/coverage/lcov.info @@ -214,7 +224,7 @@ jobs: with: name: coverage-reports-${{ matrix.name }} path: | - src/coverage/lcov.info + src/coverage/unit/lcov.info webview-ui/coverage/lcov.info packages/cloud/coverage/lcov.info packages/telemetry/coverage/lcov.info From 51297617d5b1b613ccc9f0fdbf285ea148b29d6a Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 14:17:56 +0000 Subject: [PATCH 02/28] fix(ci): prepare tree-sitter WASMs for coverage --- src/package.json | 1 + src/scripts/copy-tree-sitter-wasms.mjs | 15 +++++++++++++++ src/turbo.json | 5 ++++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/scripts/copy-tree-sitter-wasms.mjs diff --git a/src/package.json b/src/package.json index 7467b50b7b..20e4026eb5 100644 --- a/src/package.json +++ b/src/package.json @@ -442,6 +442,7 @@ "lint": "eslint . --ext=ts --max-warnings=0", "check-types": "tsc --noEmit", "test": "vitest run", + "prepare:tree-sitter-wasms": "node scripts/copy-tree-sitter-wasms.mjs", "test:unit": "vitest run --config vitest.unit.config.ts", "test:dist": "vitest run --config vitest.dist.config.ts", "test:coverage": "vitest run --coverage", diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs new file mode 100644 index 0000000000..9eab90375c --- /dev/null +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -0,0 +1,15 @@ +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const wasmDir = path.join(srcDir, "node_modules", "tree-sitter-wasms", "out") +const distDir = path.join(srcDir, "dist") + +fs.mkdirSync(distDir, { recursive: true }) + +for (const filename of fs.readdirSync(wasmDir)) { + if (filename.endsWith(".wasm")) { + fs.copyFileSync(path.join(wasmDir, filename), path.join(distDir, filename)) + } +} diff --git a/src/turbo.json b/src/turbo.json index 024971987f..8ab25798a6 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -12,11 +12,14 @@ "test:dist": { "dependsOn": ["bundle"] }, + "prepare:tree-sitter-wasms": { + "outputs": ["dist/tree-sitter-*.wasm"] + }, "test:coverage": { "dependsOn": ["$TURBO_EXTENDS$", "bundle"] }, "test:coverage:unit": { - "dependsOn": ["@roo-code/types#build"], + "dependsOn": ["@roo-code/types#build", "prepare:tree-sitter-wasms"], "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"], "outputs": ["coverage/unit/**"] }, From 54581c81f98fd4656e7bac6c51a152b4742be5ca Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 14:35:53 +0000 Subject: [PATCH 03/28] fix(ci): require covered lines in LCOV guard --- .github/workflows/code-qa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 170d1336cc..59faaed8ef 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -165,7 +165,7 @@ jobs: run: | test -s src/coverage/unit/lcov.info grep -q '^SF:' src/coverage/unit/lcov.info - grep -Eq '^LF:[1-9][0-9]*$' src/coverage/unit/lcov.info + grep -Eq '^LH:[1-9][0-9]*$' src/coverage/unit/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 From 795e8c0b259dca497b534f0f5c342a85e50eaa3c Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 15:56:04 +0000 Subject: [PATCH 04/28] fix(ci): publish coverage prerequisites atomically --- .github/workflows/code-qa.yml | 13 +++++++++++-- src/scripts/copy-tree-sitter-wasms.mjs | 11 ++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 59faaed8ef..e4c7f3674a 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -164,8 +164,17 @@ jobs: shell: bash run: | test -s src/coverage/unit/lcov.info - grep -q '^SF:' src/coverage/unit/lcov.info - grep -Eq '^LH:[1-9][0-9]*$' src/coverage/unit/lcov.info + awk ' + /^SF:/ { if (in_record) invalid=1; in_record=1; covered=0; next } + /^LH:[1-9][0-9]*$/ { if (!in_record) invalid=1; covered=1; next } + /^end_of_record$/ { + if (!in_record) invalid=1 + any_covered = any_covered || covered + in_record=0 + covered=0 + } + END { exit !(any_covered && !in_record && !invalid) } + ' src/coverage/unit/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 9eab90375c..51d55122f7 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -1,5 +1,6 @@ import fs from "node:fs" import path from "node:path" +import process from "node:process" import { fileURLToPath } from "node:url" const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") @@ -10,6 +11,14 @@ fs.mkdirSync(distDir, { recursive: true }) for (const filename of fs.readdirSync(wasmDir)) { if (filename.endsWith(".wasm")) { - fs.copyFileSync(path.join(wasmDir, filename), path.join(distDir, filename)) + const destination = path.join(distDir, filename) + const temporary = `${destination}.${process.pid}.tmp` + + try { + fs.copyFileSync(path.join(wasmDir, filename), temporary) + fs.renameSync(temporary, destination) + } finally { + fs.rmSync(temporary, { force: true }) + } } } From af97e9d6d754e68bd5a02d0a72f15e7a8f2b31a6 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 18:17:19 +0000 Subject: [PATCH 05/28] test(ci): enforce coverage lane contract --- .github/workflows/code-qa.yml | 17 +----- src/package.json | 1 + src/scripts/copy-tree-sitter-wasms.mjs | 63 +++++++++++++++++---- src/scripts/copy-tree-sitter-wasms.spec.mjs | 51 +++++++++++++++++ src/scripts/verify-coverage-contract.mjs | 51 +++++++++++++++++ src/scripts/verify-lcov.mjs | 28 +++++++++ src/scripts/verify-lcov.spec.mjs | 17 ++++++ 7 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 src/scripts/copy-tree-sitter-wasms.spec.mjs create mode 100644 src/scripts/verify-coverage-contract.mjs create mode 100644 src/scripts/verify-lcov.mjs create mode 100644 src/scripts/verify-lcov.spec.mjs diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index e4c7f3674a..1d880c71c6 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -154,6 +154,8 @@ jobs: run: pnpm turbo run test:coverage --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only - name: Run extension unit coverage run: pnpm turbo run test:coverage:unit --filter="zoo-code" --log-order grouped --output-logs new-only + - name: Verify extension coverage contract + run: pnpm --dir src verify:coverage-contract - name: Run extension dist smoke test run: pnpm turbo run test:dist --filter="zoo-code" --log-order grouped --output-logs new-only - name: Run core unit coverage @@ -161,20 +163,7 @@ jobs: - name: Run core integration coverage run: pnpm turbo run test:coverage:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Verify extension unit coverage report - shell: bash - run: | - test -s src/coverage/unit/lcov.info - awk ' - /^SF:/ { if (in_record) invalid=1; in_record=1; covered=0; next } - /^LH:[1-9][0-9]*$/ { if (!in_record) invalid=1; covered=1; next } - /^end_of_record$/ { - if (!in_record) invalid=1 - any_covered = any_covered || covered - in_record=0 - covered=0 - } - END { exit !(any_covered && !in_record && !invalid) } - ' src/coverage/unit/lcov.info + run: node src/scripts/verify-lcov.mjs src/coverage/unit/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/src/package.json b/src/package.json index 20e4026eb5..edf97d6f47 100644 --- a/src/package.json +++ b/src/package.json @@ -443,6 +443,7 @@ "check-types": "tsc --noEmit", "test": "vitest run", "prepare:tree-sitter-wasms": "node scripts/copy-tree-sitter-wasms.mjs", + "verify:coverage-contract": "node scripts/verify-coverage-contract.mjs", "test:unit": "vitest run --config vitest.unit.config.ts", "test:dist": "vitest run --config vitest.dist.config.ts", "test:coverage": "vitest run --coverage", diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 51d55122f7..56aa557449 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -6,19 +6,62 @@ import { fileURLToPath } from "node:url" const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const wasmDir = path.join(srcDir, "node_modules", "tree-sitter-wasms", "out") const distDir = path.join(srcDir, "dist") +const wasmPattern = /^tree-sitter-.*\.wasm$/ +const temporaryPattern = /^tree-sitter-.*\.wasm\.\d+\.tmp$/ -fs.mkdirSync(distDir, { recursive: true }) +export function cleanPublishedTreeSitterWasms(destinationDir, filesystem = fs) { + if (!filesystem.existsSync(destinationDir)) return + for (const filename of filesystem.readdirSync(destinationDir)) { + if (wasmPattern.test(filename) || temporaryPattern.test(filename)) { + filesystem.rmSync(path.join(destinationDir, filename), { force: true }) + } + } +} + +export function publishTreeSitterWasms(sourceDir, destinationDir, filesystem = fs) { + const sourceFiles = filesystem + .readdirSync(sourceDir) + .filter((filename) => wasmPattern.test(filename)) + .sort() + const cleanup = () => cleanPublishedTreeSitterWasms(destinationDir, filesystem) + + filesystem.mkdirSync(destinationDir, { recursive: true }) + for (const filename of filesystem.readdirSync(destinationDir)) { + if (temporaryPattern.test(filename)) filesystem.rmSync(path.join(destinationDir, filename), { force: true }) + } -for (const filename of fs.readdirSync(wasmDir)) { - if (filename.endsWith(".wasm")) { - const destination = path.join(distDir, filename) - const temporary = `${destination}.${process.pid}.tmp` + try { + for (const filename of sourceFiles) { + const destination = path.join(destinationDir, filename) + const temporary = `${destination}.${process.pid}.tmp` - try { - fs.copyFileSync(path.join(wasmDir, filename), temporary) - fs.renameSync(temporary, destination) - } finally { - fs.rmSync(temporary, { force: true }) + try { + filesystem.copyFileSync(path.join(sourceDir, filename), temporary) + filesystem.renameSync(temporary, destination) + } finally { + filesystem.rmSync(temporary, { force: true }) + } } + + for (const filename of filesystem.readdirSync(destinationDir)) { + if ((!sourceFiles.includes(filename) && wasmPattern.test(filename)) || temporaryPattern.test(filename)) { + filesystem.rmSync(path.join(destinationDir, filename), { force: true }) + } + } + } catch (error) { + cleanup() + throw error + } + + return { sourceFiles, cleanup } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => { + cleanPublishedTreeSitterWasms(distDir) + process.exit(1) + }) } + publishTreeSitterWasms(wasmDir, distDir) } diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs new file mode 100644 index 0000000000..3f7d73fe47 --- /dev/null +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -0,0 +1,51 @@ +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { afterEach, beforeEach, describe, expect, it } from "vitest" + +import { publishTreeSitterWasms } from "./copy-tree-sitter-wasms.mjs" + +describe("publishTreeSitterWasms", () => { + let root + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasms-")) + }) + + afterEach(() => fs.rmSync(root, { recursive: true, force: true })) + + it("publishes the exact WASM set and removes stale outputs", () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + fs.writeFileSync(path.join(source, "ignored.txt"), "ignored") + fs.writeFileSync(path.join(destination, "tree-sitter-stale.wasm"), "stale") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm.999.tmp"), "partial") + + publishTreeSitterWasms(source, destination) + + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") + }) + + it("removes published and temporary outputs when publication fails", () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") + let copies = 0 + const filesystem = { + ...fs, + copyFileSync(...args) { + if (++copies === 2) throw new Error("copy failed") + return fs.copyFileSync(...args) + }, + } + + expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("copy failed") + expect(fs.readdirSync(destination)).toEqual([]) + }) +}) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs new file mode 100644 index 0000000000..d894cfe4b0 --- /dev/null +++ b/src/scripts/verify-coverage-contract.mjs @@ -0,0 +1,51 @@ +import { spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import process from "node:process" +import { fileURLToPath } from "node:url" + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") +const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm" +const run = (args, options = {}) => { + const { includeStderr = true, ...spawnOptions } = options + const result = spawnSync(pnpm, args, { cwd: root, encoding: "utf8", ...spawnOptions }) + if (result.status !== 0) throw new Error(result.stderr || result.stdout || `${pnpm} failed`) + return `${result.stdout || ""}${includeStderr ? result.stderr || "" : ""}` +} + +const graph = JSON.parse( + run(["turbo", "run", "test:coverage:unit", "--filter=zoo-code", "--dry=json"], { includeStderr: false }), +) +const coverageTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:unit") +const preparationTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms") +if (!coverageTask?.dependencies.includes("zoo-code#prepare:tree-sitter-wasms")) + throw new Error("WASM prerequisite missing") +if (coverageTask.dependencies.includes("zoo-code#bundle")) throw new Error("Unit coverage must not depend on bundle") +if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitter-*.wasm"])) + throw new Error("WASM prerequisite outputs changed") + +const dist = path.join(root, "src", "dist") +fs.mkdirSync(dist, { recursive: true }) +for (const filename of fs.readdirSync(dist)) { + if (/^tree-sitter-.*\.wasm(?:\.\d+\.tmp)?$/.test(filename)) fs.rmSync(path.join(dist, filename), { force: true }) +} + +run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--force"]) +run(["--dir", "src", "exec", "vitest", "run", "services/tree-sitter/__tests__"], { stdio: "inherit" }) + +const source = fs + .readdirSync(path.join(root, "src", "node_modules", "tree-sitter-wasms", "out")) + .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) + .sort() +const published = fs + .readdirSync(dist) + .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) + .sort() +if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") +if (JSON.stringify(source) !== JSON.stringify(published)) + throw new Error("Published WASM set does not match dependency") +if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) throw new Error("Temporary WASM files remain") + +const warm = run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code"]) +if (!warm.includes("zoo-code:prepare:tree-sitter-wasms: cache hit")) + throw new Error("WASM prerequisite did not restore from cache") diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs new file mode 100644 index 0000000000..06601b1cac --- /dev/null +++ b/src/scripts/verify-lcov.mjs @@ -0,0 +1,28 @@ +import fs from "node:fs" +import process from "node:process" +import { fileURLToPath } from "node:url" + +export function verifyLcov(content) { + let inRecord = false + let anyCovered = false + + for (const line of content.split(/\r?\n/)) { + if (line.startsWith("SF:")) { + if (inRecord) throw new Error("LCOV source record is not terminated") + inRecord = true + } else if (line.startsWith("LH:")) { + if (!inRecord) throw new Error("LCOV hit count is outside a source record") + if (Number(line.slice(3)) > 0) anyCovered = true + } else if (line === "end_of_record") { + if (!inRecord) throw new Error("LCOV terminator is outside a source record") + inRecord = false + } + } + + if (inRecord) throw new Error("LCOV source record is not terminated") + if (!anyCovered) throw new Error("LCOV report has no covered lines") +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + verifyLcov(fs.readFileSync(process.argv[2], "utf8")) +} diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs new file mode 100644 index 0000000000..a076bdd523 --- /dev/null +++ b/src/scripts/verify-lcov.spec.mjs @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest" + +import { verifyLcov } from "./verify-lcov.mjs" + +describe("verifyLcov", () => { + it("accepts complete records with covered lines", () => { + expect(() => verifyLcov("SF:file.ts\nLF:1\nLH:1\nend_of_record\n")).not.toThrow() + }) + + it.each([ + ["an unterminated record", "SF:file.ts\nLH:1\n"], + ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], + ["a hit count outside a record", "LH:1\n"], + ])("rejects %s", (_, content) => { + expect(() => verifyLcov(content)).toThrow() + }) +}) From d6a67e9175f9a049a0cfcfe93269c047ae5b2115 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 18:24:20 +0000 Subject: [PATCH 06/28] fix(ci): isolate coverage contract cache --- .github/workflows/code-qa.yml | 2 +- src/scripts/verify-coverage-contract.mjs | 47 +++++++++++++++--------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 1d880c71c6..8c2d45d605 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -155,7 +155,7 @@ jobs: - name: Run extension unit coverage run: pnpm turbo run test:coverage:unit --filter="zoo-code" --log-order grouped --output-logs new-only - name: Verify extension coverage contract - run: pnpm --dir src verify:coverage-contract + run: pnpm --dir src run verify:coverage-contract - name: Run extension dist smoke test run: pnpm turbo run test:dist --filter="zoo-code" --log-order grouped --output-logs new-only - name: Run core unit coverage diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index d894cfe4b0..002ea6ea61 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -25,27 +25,40 @@ if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitt throw new Error("WASM prerequisite outputs changed") const dist = path.join(root, "src", "dist") +const cacheDir = path.join(root, ".turbo", "coverage-contract") fs.mkdirSync(dist, { recursive: true }) +fs.rmSync(cacheDir, { recursive: true, force: true }) for (const filename of fs.readdirSync(dist)) { if (/^tree-sitter-.*\.wasm(?:\.\d+\.tmp)?$/.test(filename)) fs.rmSync(path.join(dist, filename), { force: true }) } -run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--force"]) -run(["--dir", "src", "exec", "vitest", "run", "services/tree-sitter/__tests__"], { stdio: "inherit" }) +try { + run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) + run(["--dir", "src", "exec", "vitest", "run", "services/tree-sitter/__tests__"], { stdio: "inherit" }) -const source = fs - .readdirSync(path.join(root, "src", "node_modules", "tree-sitter-wasms", "out")) - .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) - .sort() -const published = fs - .readdirSync(dist) - .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) - .sort() -if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") -if (JSON.stringify(source) !== JSON.stringify(published)) - throw new Error("Published WASM set does not match dependency") -if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) throw new Error("Temporary WASM files remain") + const source = fs + .readdirSync(path.join(root, "src", "node_modules", "tree-sitter-wasms", "out")) + .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) + .sort() + const published = fs + .readdirSync(dist) + .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) + .sort() + if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") + if (JSON.stringify(source) !== JSON.stringify(published)) + throw new Error("Published WASM set does not match dependency") + if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) + throw new Error("Temporary WASM files remain") -const warm = run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code"]) -if (!warm.includes("zoo-code:prepare:tree-sitter-wasms: cache hit")) - throw new Error("WASM prerequisite did not restore from cache") + const warm = run([ + "turbo", + "run", + "prepare:tree-sitter-wasms", + "--filter=zoo-code", + "--cache-dir=.turbo/coverage-contract", + ]) + if (!warm.includes("zoo-code:prepare:tree-sitter-wasms: cache hit")) + throw new Error("WASM prerequisite did not restore from cache") +} finally { + fs.rmSync(cacheDir, { recursive: true, force: true }) +} From 89a338ef0ce7ec6bc197be0c7746f7343984fdd8 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 18:30:32 +0000 Subject: [PATCH 07/28] fix(ci): verify cached WASM restoration --- src/scripts/verify-coverage-contract.mjs | 31 +++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 002ea6ea61..f7a71d6f11 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -50,15 +50,28 @@ try { if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) throw new Error("Temporary WASM files remain") - const warm = run([ - "turbo", - "run", - "prepare:tree-sitter-wasms", - "--filter=zoo-code", - "--cache-dir=.turbo/coverage-contract", - ]) - if (!warm.includes("zoo-code:prepare:tree-sitter-wasms: cache hit")) - throw new Error("WASM prerequisite did not restore from cache") + for (const filename of published) fs.rmSync(path.join(dist, filename), { force: true }) + const warmGraph = JSON.parse( + run( + [ + "turbo", + "run", + "prepare:tree-sitter-wasms", + "--filter=zoo-code", + "--cache-dir=.turbo/coverage-contract", + "--dry=json", + ], + { includeStderr: false }, + ), + ) + const warmTask = warmGraph.tasks.find(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms") + if (warmTask?.cache.status !== "HIT") throw new Error("WASM prerequisite is not available in the isolated cache") + run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) + const restored = fs + .readdirSync(dist) + .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) + .sort() + if (JSON.stringify(source) !== JSON.stringify(restored)) throw new Error("WASM cache did not restore exact outputs") } finally { fs.rmSync(cacheDir, { recursive: true, force: true }) } From 75d19fa3099002e04f42d1c2b38442ffc647a40c Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 18:50:20 +0000 Subject: [PATCH 08/28] fix(ci): support coverage verifier on Windows --- src/scripts/copy-tree-sitter-wasms.spec.mjs | 16 ++++++++++++++++ src/scripts/verify-coverage-contract.mjs | 19 ++++++++++++++++--- src/scripts/verify-lcov.mjs | 4 +++- src/scripts/verify-lcov.spec.mjs | 4 ++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 3f7d73fe47..d0fbcb87cd 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -48,4 +48,20 @@ describe("publishTreeSitterWasms", () => { expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("copy failed") expect(fs.readdirSync(destination)).toEqual([]) }) + + it("removes published and temporary outputs when an atomic rename fails", () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + const filesystem = { + ...fs, + renameSync() { + throw new Error("rename failed") + }, + } + + expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("rename failed") + expect(fs.readdirSync(destination)).toEqual([]) + }) }) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index f7a71d6f11..7b5bf3262e 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -5,11 +5,24 @@ import process from "node:process" import { fileURLToPath } from "node:url" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") -const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm" +const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" +if (!pnpm) throw new Error("pnpm executable path is unavailable") +const pnpmPrefix = process.platform === "win32" ? [pnpm] : [] const run = (args, options = {}) => { const { includeStderr = true, ...spawnOptions } = options - const result = spawnSync(pnpm, args, { cwd: root, encoding: "utf8", ...spawnOptions }) - if (result.status !== 0) throw new Error(result.stderr || result.stdout || `${pnpm} failed`) + const command = process.platform === "win32" ? process.execPath : pnpm + const result = spawnSync(command, [...pnpmPrefix, ...args], { cwd: root, encoding: "utf8", ...spawnOptions }) + if (result.status !== 0) { + const details = [ + result.error?.message, + result.signal ? `terminated by ${result.signal}` : undefined, + result.stderr, + result.stdout, + ] + .filter(Boolean) + .join("\n") + throw new Error(details || `pnpm exited with status ${result.status ?? "unknown"}`) + } return `${result.stdout || ""}${includeStderr ? result.stderr || "" : ""}` } diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs index 06601b1cac..675cbffabe 100644 --- a/src/scripts/verify-lcov.mjs +++ b/src/scripts/verify-lcov.mjs @@ -12,7 +12,9 @@ export function verifyLcov(content) { inRecord = true } else if (line.startsWith("LH:")) { if (!inRecord) throw new Error("LCOV hit count is outside a source record") - if (Number(line.slice(3)) > 0) anyCovered = true + const hits = line.slice(3) + if (!/^\d+$/.test(hits)) throw new Error("LCOV hit count is not a decimal integer") + if (BigInt(hits) > 0n) anyCovered = true } else if (line === "end_of_record") { if (!inRecord) throw new Error("LCOV terminator is outside a source record") inRecord = false diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index a076bdd523..26aa673371 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -11,6 +11,10 @@ describe("verifyLcov", () => { ["an unterminated record", "SF:file.ts\nLH:1\n"], ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], ["a hit count outside a record", "LH:1\n"], + ["a terminator outside a record", "end_of_record\n"], + ["an infinite hit count", "SF:file.ts\nLH:Infinity\nend_of_record\n"], + ["a fractional hit count", "SF:file.ts\nLH:1.5\nend_of_record\n"], + ["an exponential hit count", "SF:file.ts\nLH:1e3\nend_of_record\n"], ])("rejects %s", (_, content) => { expect(() => verifyLcov(content)).toThrow() }) From f69e1fc4629854316e9ecd6a9b0d8827d84b28e9 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 19:12:19 +0000 Subject: [PATCH 09/28] fix(ci): require complete LCOV line summaries --- src/scripts/verify-lcov.mjs | 10 ++++++++++ src/scripts/verify-lcov.spec.mjs | 1 + 2 files changed, 11 insertions(+) diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs index 675cbffabe..3324bd1fa8 100644 --- a/src/scripts/verify-lcov.mjs +++ b/src/scripts/verify-lcov.mjs @@ -5,18 +5,28 @@ import { fileURLToPath } from "node:url" export function verifyLcov(content) { let inRecord = false let anyCovered = false + let hasLinesFound = false + let hasLinesHit = false for (const line of content.split(/\r?\n/)) { if (line.startsWith("SF:")) { if (inRecord) throw new Error("LCOV source record is not terminated") inRecord = true + hasLinesFound = false + hasLinesHit = false + } else if (line.startsWith("LF:")) { + if (!inRecord) throw new Error("LCOV line count is outside a source record") + if (!/^\d+$/.test(line.slice(3))) throw new Error("LCOV line count is not a decimal integer") + hasLinesFound = true } else if (line.startsWith("LH:")) { if (!inRecord) throw new Error("LCOV hit count is outside a source record") const hits = line.slice(3) if (!/^\d+$/.test(hits)) throw new Error("LCOV hit count is not a decimal integer") if (BigInt(hits) > 0n) anyCovered = true + hasLinesHit = true } else if (line === "end_of_record") { if (!inRecord) throw new Error("LCOV terminator is outside a source record") + if (!hasLinesFound || !hasLinesHit) throw new Error("LCOV source record has incomplete line summaries") inRecord = false } } diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index 26aa673371..3d523f38e1 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -12,6 +12,7 @@ describe("verifyLcov", () => { ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], ["a hit count outside a record", "LH:1\n"], ["a terminator outside a record", "end_of_record\n"], + ["a record without lines found", "SF:file.ts\nLH:1\nend_of_record\n"], ["an infinite hit count", "SF:file.ts\nLH:Infinity\nend_of_record\n"], ["a fractional hit count", "SF:file.ts\nLH:1.5\nend_of_record\n"], ["an exponential hit count", "SF:file.ts\nLH:1e3\nend_of_record\n"], From 55c60fce6e3cfafc006f4f8fe5d661360e899f99 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 21:38:42 +0000 Subject: [PATCH 10/28] fix(ci): validate LCOV line totals --- src/scripts/verify-lcov.mjs | 21 ++++++++++++--------- src/scripts/verify-lcov.spec.mjs | 4 ++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs index 3324bd1fa8..b073a26dd6 100644 --- a/src/scripts/verify-lcov.mjs +++ b/src/scripts/verify-lcov.mjs @@ -5,28 +5,31 @@ import { fileURLToPath } from "node:url" export function verifyLcov(content) { let inRecord = false let anyCovered = false - let hasLinesFound = false - let hasLinesHit = false + let linesFound + let linesHit for (const line of content.split(/\r?\n/)) { if (line.startsWith("SF:")) { if (inRecord) throw new Error("LCOV source record is not terminated") inRecord = true - hasLinesFound = false - hasLinesHit = false + linesFound = undefined + linesHit = undefined } else if (line.startsWith("LF:")) { if (!inRecord) throw new Error("LCOV line count is outside a source record") - if (!/^\d+$/.test(line.slice(3))) throw new Error("LCOV line count is not a decimal integer") - hasLinesFound = true + const found = line.slice(3) + if (!/^\d+$/.test(found)) throw new Error("LCOV line count is not a decimal integer") + linesFound = BigInt(found) } else if (line.startsWith("LH:")) { if (!inRecord) throw new Error("LCOV hit count is outside a source record") const hits = line.slice(3) if (!/^\d+$/.test(hits)) throw new Error("LCOV hit count is not a decimal integer") - if (BigInt(hits) > 0n) anyCovered = true - hasLinesHit = true + linesHit = BigInt(hits) + if (linesHit > 0n) anyCovered = true } else if (line === "end_of_record") { if (!inRecord) throw new Error("LCOV terminator is outside a source record") - if (!hasLinesFound || !hasLinesHit) throw new Error("LCOV source record has incomplete line summaries") + if (linesFound === undefined || linesHit === undefined) + throw new Error("LCOV source record has incomplete line summaries") + if (linesHit > linesFound) throw new Error("LCOV hit count exceeds lines found") inRecord = false } } diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index 3d523f38e1..9b3966fdae 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -13,9 +13,13 @@ describe("verifyLcov", () => { ["a hit count outside a record", "LH:1\n"], ["a terminator outside a record", "end_of_record\n"], ["a record without lines found", "SF:file.ts\nLH:1\nend_of_record\n"], + ["an infinite line count", "SF:file.ts\nLF:Infinity\nLH:1\nend_of_record\n"], + ["a fractional line count", "SF:file.ts\nLF:1.5\nLH:1\nend_of_record\n"], + ["an exponential line count", "SF:file.ts\nLF:1e3\nLH:1\nend_of_record\n"], ["an infinite hit count", "SF:file.ts\nLH:Infinity\nend_of_record\n"], ["a fractional hit count", "SF:file.ts\nLH:1.5\nend_of_record\n"], ["an exponential hit count", "SF:file.ts\nLH:1e3\nend_of_record\n"], + ["more hit lines than found lines", "SF:file.ts\nLF:0\nLH:1\nend_of_record\n"], ])("rejects %s", (_, content) => { expect(() => verifyLcov(content)).toThrow() }) From dd42be5e8fa01a4bed2ccc318c52f4e62bace372 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 12 Sep 2026 23:38:12 +0000 Subject: [PATCH 11/28] fix(ci): reject duplicate LCOV summaries --- src/scripts/verify-lcov.mjs | 4 +++- src/scripts/verify-lcov.spec.mjs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs index b073a26dd6..303a4de52b 100644 --- a/src/scripts/verify-lcov.mjs +++ b/src/scripts/verify-lcov.mjs @@ -16,20 +16,22 @@ export function verifyLcov(content) { linesHit = undefined } else if (line.startsWith("LF:")) { if (!inRecord) throw new Error("LCOV line count is outside a source record") + if (linesFound !== undefined) throw new Error("LCOV source record has duplicate line counts") const found = line.slice(3) if (!/^\d+$/.test(found)) throw new Error("LCOV line count is not a decimal integer") linesFound = BigInt(found) } else if (line.startsWith("LH:")) { if (!inRecord) throw new Error("LCOV hit count is outside a source record") + if (linesHit !== undefined) throw new Error("LCOV source record has duplicate hit counts") const hits = line.slice(3) if (!/^\d+$/.test(hits)) throw new Error("LCOV hit count is not a decimal integer") linesHit = BigInt(hits) - if (linesHit > 0n) anyCovered = true } else if (line === "end_of_record") { if (!inRecord) throw new Error("LCOV terminator is outside a source record") if (linesFound === undefined || linesHit === undefined) throw new Error("LCOV source record has incomplete line summaries") if (linesHit > linesFound) throw new Error("LCOV hit count exceeds lines found") + if (linesHit > 0n) anyCovered = true inRecord = false } } diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index 9b3966fdae..84198feae6 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -12,6 +12,7 @@ describe("verifyLcov", () => { ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], ["a hit count outside a record", "LH:1\n"], ["a terminator outside a record", "end_of_record\n"], + ["consecutive source records", "SF:first.ts\nSF:second.ts\nLF:1\nLH:1\nend_of_record\n"], ["a record without lines found", "SF:file.ts\nLH:1\nend_of_record\n"], ["an infinite line count", "SF:file.ts\nLF:Infinity\nLH:1\nend_of_record\n"], ["a fractional line count", "SF:file.ts\nLF:1.5\nLH:1\nend_of_record\n"], @@ -19,6 +20,8 @@ describe("verifyLcov", () => { ["an infinite hit count", "SF:file.ts\nLH:Infinity\nend_of_record\n"], ["a fractional hit count", "SF:file.ts\nLH:1.5\nend_of_record\n"], ["an exponential hit count", "SF:file.ts\nLH:1e3\nend_of_record\n"], + ["duplicate line counts", "SF:file.ts\nLF:1\nLF:0\nLH:0\nend_of_record\n"], + ["duplicate hit counts", "SF:file.ts\nLF:1\nLH:1\nLH:0\nend_of_record\n"], ["more hit lines than found lines", "SF:file.ts\nLF:0\nLH:1\nend_of_record\n"], ])("rejects %s", (_, content) => { expect(() => verifyLcov(content)).toThrow() From a857936b0c29b38b396c6c4920878415e1e05303 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 00:40:34 +0000 Subject: [PATCH 12/28] fix(ci): reject empty LCOV source paths --- src/scripts/verify-lcov.mjs | 1 + src/scripts/verify-lcov.spec.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/scripts/verify-lcov.mjs b/src/scripts/verify-lcov.mjs index 303a4de52b..c4220dbf00 100644 --- a/src/scripts/verify-lcov.mjs +++ b/src/scripts/verify-lcov.mjs @@ -11,6 +11,7 @@ export function verifyLcov(content) { for (const line of content.split(/\r?\n/)) { if (line.startsWith("SF:")) { if (inRecord) throw new Error("LCOV source record is not terminated") + if (!line.slice(3)) throw new Error("LCOV source path is empty") inRecord = true linesFound = undefined linesHit = undefined diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index 84198feae6..9060dc6a44 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -8,6 +8,7 @@ describe("verifyLcov", () => { }) it.each([ + ["an empty source path", "SF:\nLF:1\nLH:1\nend_of_record\n"], ["an unterminated record", "SF:file.ts\nLH:1\n"], ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], ["a hit count outside a record", "LH:1\n"], From e38cb20e463358fc2794d10ea61ad8bbbdfdbdf7 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 01:34:06 +0000 Subject: [PATCH 13/28] fix(ci): preserve verified WASM artifacts --- src/scripts/copy-tree-sitter-wasms.mjs | 24 ++++++++++++----- src/scripts/copy-tree-sitter-wasms.spec.mjs | 23 +++++++++++----- src/scripts/verify-coverage-contract.mjs | 14 ++++++++++ src/scripts/verify-wasm-files.mjs | 14 ++++++++++ src/scripts/verify-wasm-files.spec.mjs | 29 +++++++++++++++++++++ 5 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 src/scripts/verify-wasm-files.mjs create mode 100644 src/scripts/verify-wasm-files.spec.mjs diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 56aa557449..918c965d05 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -26,21 +26,26 @@ export function publishTreeSitterWasms(sourceDir, destinationDir, filesystem = f const cleanup = () => cleanPublishedTreeSitterWasms(destinationDir, filesystem) filesystem.mkdirSync(destinationDir, { recursive: true }) + const previousFiles = new Map( + filesystem + .readdirSync(destinationDir) + .filter((filename) => wasmPattern.test(filename)) + .map((filename) => [filename, filesystem.readFileSync(path.join(destinationDir, filename))]), + ) for (const filename of filesystem.readdirSync(destinationDir)) { if (temporaryPattern.test(filename)) filesystem.rmSync(path.join(destinationDir, filename), { force: true }) } + const temporaryFiles = [] try { for (const filename of sourceFiles) { const destination = path.join(destinationDir, filename) const temporary = `${destination}.${process.pid}.tmp` - - try { - filesystem.copyFileSync(path.join(sourceDir, filename), temporary) - filesystem.renameSync(temporary, destination) - } finally { - filesystem.rmSync(temporary, { force: true }) - } + temporaryFiles.push(temporary) + filesystem.copyFileSync(path.join(sourceDir, filename), temporary) + } + for (const [index, filename] of sourceFiles.entries()) { + filesystem.renameSync(temporaryFiles[index], path.join(destinationDir, filename)) } for (const filename of filesystem.readdirSync(destinationDir)) { @@ -50,7 +55,12 @@ export function publishTreeSitterWasms(sourceDir, destinationDir, filesystem = f } } catch (error) { cleanup() + for (const [filename, content] of previousFiles) { + filesystem.writeFileSync(path.join(destinationDir, filename), content) + } throw error + } finally { + for (const temporary of temporaryFiles) filesystem.rmSync(temporary, { force: true }) } return { sourceFiles, cleanup } diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index d0fbcb87cd..1c1cfdcad7 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -30,12 +30,14 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") }) - it("removes published and temporary outputs when publication fails", () => { + it("restores published outputs when publication fails", () => { const source = path.join(root, "source") const destination = path.join(root, "dist") fs.mkdirSync(source) + fs.mkdirSync(destination) fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") + fs.writeFileSync(path.join(destination, "tree-sitter-existing.wasm"), "existing") let copies = 0 const filesystem = { ...fs, @@ -46,22 +48,31 @@ describe("publishTreeSitterWasms", () => { } expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("copy failed") - expect(fs.readdirSync(destination)).toEqual([]) + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-existing.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-existing.wasm"), "utf8")).toBe("existing") }) - it("removes published and temporary outputs when an atomic rename fails", () => { + it("restores published outputs when an atomic rename fails", () => { const source = path.join(root, "source") const destination = path.join(root, "dist") fs.mkdirSync(source) + fs.mkdirSync(destination) fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") + let renames = 0 const filesystem = { ...fs, - renameSync() { - throw new Error("rename failed") + renameSync(...args) { + if (++renames === 2) throw new Error("rename failed") + return fs.renameSync(...args) }, } expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("rename failed") - expect(fs.readdirSync(destination)).toEqual([]) + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") }) }) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 7b5bf3262e..866b23a91e 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -4,6 +4,8 @@ import path from "node:path" import process from "node:process" import { fileURLToPath } from "node:url" +import { assertMatchingFiles } from "./verify-wasm-files.mjs" + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" if (!pnpm) throw new Error("pnpm executable path is unavailable") @@ -60,6 +62,12 @@ try { if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") if (JSON.stringify(source) !== JSON.stringify(published)) throw new Error("Published WASM set does not match dependency") + assertMatchingFiles( + path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), + dist, + source, + "Published WASM content does not match dependency", + ) if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) throw new Error("Temporary WASM files remain") @@ -85,6 +93,12 @@ try { .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) .sort() if (JSON.stringify(source) !== JSON.stringify(restored)) throw new Error("WASM cache did not restore exact outputs") + assertMatchingFiles( + path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), + dist, + source, + "WASM cache restored corrupted output", + ) } finally { fs.rmSync(cacheDir, { recursive: true, force: true }) } diff --git a/src/scripts/verify-wasm-files.mjs b/src/scripts/verify-wasm-files.mjs new file mode 100644 index 0000000000..73ef3737da --- /dev/null +++ b/src/scripts/verify-wasm-files.mjs @@ -0,0 +1,14 @@ +import fs from "node:fs" +import path from "node:path" + +export function assertMatchingFiles(expectedDir, actualDir, filenames, message, filesystem = fs) { + for (const filename of filenames) { + if ( + !filesystem + .readFileSync(path.join(expectedDir, filename)) + .equals(filesystem.readFileSync(path.join(actualDir, filename))) + ) { + throw new Error(`${message}: ${filename}`) + } + } +} diff --git a/src/scripts/verify-wasm-files.spec.mjs b/src/scripts/verify-wasm-files.spec.mjs new file mode 100644 index 0000000000..3beedc642d --- /dev/null +++ b/src/scripts/verify-wasm-files.spec.mjs @@ -0,0 +1,29 @@ +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { afterEach, beforeEach, describe, expect, it } from "vitest" + +import { assertMatchingFiles } from "./verify-wasm-files.mjs" + +describe("assertMatchingFiles", () => { + let root + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "verify-wasm-files-")) + }) + + afterEach(() => fs.rmSync(root, { recursive: true, force: true })) + + it("rejects a restored WASM with corrupted content", () => { + const expected = path.join(root, "expected") + const actual = path.join(root, "actual") + fs.mkdirSync(expected) + fs.mkdirSync(actual) + fs.writeFileSync(path.join(expected, "tree-sitter-a.wasm"), "expected") + fs.writeFileSync(path.join(actual, "tree-sitter-a.wasm"), "corrupted") + + expect(() => assertMatchingFiles(expected, actual, ["tree-sitter-a.wasm"], "WASM mismatch")).toThrow( + "WASM mismatch: tree-sitter-a.wasm", + ) + }) +}) From 6e396777a7bd512f17a014a19ac3bca9d0422792 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 01:52:19 +0000 Subject: [PATCH 14/28] test(ci): cover LCOV line count placement --- src/scripts/verify-lcov.spec.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/verify-lcov.spec.mjs b/src/scripts/verify-lcov.spec.mjs index 9060dc6a44..849c722979 100644 --- a/src/scripts/verify-lcov.spec.mjs +++ b/src/scripts/verify-lcov.spec.mjs @@ -11,6 +11,7 @@ describe("verifyLcov", () => { ["an empty source path", "SF:\nLF:1\nLH:1\nend_of_record\n"], ["an unterminated record", "SF:file.ts\nLH:1\n"], ["a zero-hit report", "SF:file.ts\nLF:1\nLH:0\nend_of_record\n"], + ["a line count outside a record", "LF:1\n"], ["a hit count outside a record", "LH:1\n"], ["a terminator outside a record", "end_of_record\n"], ["consecutive source records", "SF:first.ts\nSF:second.ts\nLF:1\nLH:1\nend_of_record\n"], From e20f66d684ee9455186c8bc649c3bb004cc2a839 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 02:23:45 +0000 Subject: [PATCH 15/28] fix(ci): make WASM publication transactional --- src/scripts/copy-tree-sitter-wasms.mjs | 115 +++++++++++++------- src/scripts/copy-tree-sitter-wasms.spec.mjs | 80 +++++++++++--- src/scripts/verify-coverage-contract.mjs | 6 + 3 files changed, 150 insertions(+), 51 deletions(-) diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 918c965d05..f86bcca2d5 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -18,60 +18,99 @@ export function cleanPublishedTreeSitterWasms(destinationDir, filesystem = fs) { } } -export function publishTreeSitterWasms(sourceDir, destinationDir, filesystem = fs) { - const sourceFiles = filesystem - .readdirSync(sourceDir) - .filter((filename) => wasmPattern.test(filename)) - .sort() - const cleanup = () => cleanPublishedTreeSitterWasms(destinationDir, filesystem) - - filesystem.mkdirSync(destinationDir, { recursive: true }) - const previousFiles = new Map( - filesystem - .readdirSync(destinationDir) - .filter((filename) => wasmPattern.test(filename)) - .map((filename) => [filename, filesystem.readFileSync(path.join(destinationDir, filename))]), - ) - for (const filename of filesystem.readdirSync(destinationDir)) { - if (temporaryPattern.test(filename)) filesystem.rmSync(path.join(destinationDir, filename), { force: true }) +export async function publishTreeSitterWasms( + sourceDir, + destinationDir, + { filesystem = fs.promises, onStep = async () => {}, signalState = { requested: undefined } } = {}, +) { + const transactionDir = `${destinationDir}.tree-sitter-wasms-transaction` + const stagedDir = path.join(transactionDir, "staged") + const backupDir = path.join(transactionDir, "backup") + const quarantineDir = path.join(transactionDir, "quarantine") + const sourceFiles = (await filesystem.readdir(sourceDir)).filter((filename) => wasmPattern.test(filename)).sort() + const step = async (name, filename) => { + await onStep(name, filename) + if (signalState.requested) throw Object.assign(new Error("WASM publication cancelled"), { code: "CANCELLED" }) } - const temporaryFiles = [] + await filesystem.mkdir(destinationDir, { recursive: true }) + await filesystem.mkdir(transactionDir) + await filesystem.mkdir(stagedDir) + await filesystem.mkdir(backupDir) + await filesystem.mkdir(quarantineDir) + + let commitStarted = false + const publishedFiles = [] try { for (const filename of sourceFiles) { - const destination = path.join(destinationDir, filename) - const temporary = `${destination}.${process.pid}.tmp` - temporaryFiles.push(temporary) - filesystem.copyFileSync(path.join(sourceDir, filename), temporary) + await filesystem.copyFile(path.join(sourceDir, filename), path.join(stagedDir, filename)) + await step("staged", filename) } - for (const [index, filename] of sourceFiles.entries()) { - filesystem.renameSync(temporaryFiles[index], path.join(destinationDir, filename)) + + const previousFiles = (await filesystem.readdir(destinationDir)).filter((filename) => + wasmPattern.test(filename), + ) + commitStarted = true + for (const filename of previousFiles) { + await filesystem.rename(path.join(destinationDir, filename), path.join(backupDir, filename)) + await step("backed-up", filename) + } + for (const filename of sourceFiles) { + await filesystem.rename(path.join(stagedDir, filename), path.join(destinationDir, filename)) + publishedFiles.push(filename) + await step("published", filename) + } + for (const filename of await filesystem.readdir(destinationDir)) { + if (temporaryPattern.test(filename)) + await filesystem.rm(path.join(destinationDir, filename), { force: true }) } - for (const filename of filesystem.readdirSync(destinationDir)) { - if ((!sourceFiles.includes(filename) && wasmPattern.test(filename)) || temporaryPattern.test(filename)) { - filesystem.rmSync(path.join(destinationDir, filename), { force: true }) + await filesystem.rm(transactionDir, { recursive: true, force: true }) + return { sourceFiles, cleanup: () => cleanPublishedTreeSitterWasms(destinationDir) } + } catch (error) { + if (!commitStarted) { + await filesystem.rm(transactionDir, { recursive: true, force: true }) + throw error + } + + const failures = [] + for (const filename of publishedFiles) { + try { + await filesystem.rename(path.join(destinationDir, filename), path.join(quarantineDir, filename)) + } catch (rollbackError) { + if (rollbackError.code !== "ENOENT") failures.push(rollbackError) } } - } catch (error) { - cleanup() - for (const [filename, content] of previousFiles) { - filesystem.writeFileSync(path.join(destinationDir, filename), content) + for (const filename of await filesystem.readdir(backupDir)) { + try { + await filesystem.rename(path.join(backupDir, filename), path.join(destinationDir, filename)) + await onStep("restored", filename) + } catch (rollbackError) { + failures.push(rollbackError) + } + } + if (failures.length > 0) { + throw new AggregateError( + [error, ...failures], + `WASM rollback incomplete; recovery retained at ${transactionDir}`, + ) } + await filesystem.rm(transactionDir, { recursive: true, force: true }) throw error - } finally { - for (const temporary of temporaryFiles) filesystem.rmSync(temporary, { force: true }) } - - return { sourceFiles, cleanup } } if (process.argv[1] === fileURLToPath(import.meta.url)) { + const signalState = { requested: undefined } for (const signal of ["SIGINT", "SIGTERM"]) { - process.once(signal, () => { - cleanPublishedTreeSitterWasms(distDir) - process.exit(1) + process.on(signal, () => { + signalState.requested ??= signal }) } - publishTreeSitterWasms(wasmDir, distDir) + try { + await publishTreeSitterWasms(wasmDir, distDir, { signalState }) + } catch (error) { + if (!signalState.requested) throw error + process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 + } } diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 1c1cfdcad7..38875ed64b 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -14,7 +14,7 @@ describe("publishTreeSitterWasms", () => { afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - it("publishes the exact WASM set and removes stale outputs", () => { + it("publishes the exact WASM set and removes stale outputs", async () => { const source = path.join(root, "source") const destination = path.join(root, "dist") fs.mkdirSync(source) @@ -24,13 +24,13 @@ describe("publishTreeSitterWasms", () => { fs.writeFileSync(path.join(destination, "tree-sitter-stale.wasm"), "stale") fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm.999.tmp"), "partial") - publishTreeSitterWasms(source, destination) + await publishTreeSitterWasms(source, destination) expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") }) - it("restores published outputs when publication fails", () => { + it("restores published outputs when publication fails", async () => { const source = path.join(root, "source") const destination = path.join(root, "dist") fs.mkdirSync(source) @@ -40,19 +40,19 @@ describe("publishTreeSitterWasms", () => { fs.writeFileSync(path.join(destination, "tree-sitter-existing.wasm"), "existing") let copies = 0 const filesystem = { - ...fs, - copyFileSync(...args) { + ...fs.promises, + copyFile(...args) { if (++copies === 2) throw new Error("copy failed") - return fs.copyFileSync(...args) + return fs.promises.copyFile(...args) }, } - expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("copy failed") + await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("copy failed") expect(fs.readdirSync(destination)).toEqual(["tree-sitter-existing.wasm"]) expect(fs.readFileSync(path.join(destination, "tree-sitter-existing.wasm"), "utf8")).toBe("existing") }) - it("restores published outputs when an atomic rename fails", () => { + it("restores published outputs when an atomic rename fails", async () => { const source = path.join(root, "source") const destination = path.join(root, "dist") fs.mkdirSync(source) @@ -63,16 +63,70 @@ describe("publishTreeSitterWasms", () => { fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") let renames = 0 const filesystem = { - ...fs, - renameSync(...args) { - if (++renames === 2) throw new Error("rename failed") - return fs.renameSync(...args) + ...fs.promises, + rename(...args) { + if (args[0].includes(`${path.sep}staged${path.sep}`) && ++renames === 2) + throw new Error("rename failed") + return fs.promises.rename(...args) }, } - expect(() => publishTreeSitterWasms(source, destination, filesystem)).toThrow("rename failed") + await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("rename failed") expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") }) + + it("restores published outputs when signalled during commit", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + const signalState = { requested: undefined } + + await expect( + publishTreeSitterWasms(source, destination, { + signalState, + onStep(name) { + if (name === "published") signalState.requested = "SIGTERM" + if (name === "restored") signalState.requested ??= "SIGINT" + }, + }), + ).rejects.toThrow("WASM publication cancelled") + expect(signalState.requested).toBe("SIGTERM") + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + expect(fs.existsSync(`${destination}.tree-sitter-wasms-transaction`)).toBe(false) + }) + + it("retains the backup when restoration fails", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + const transaction = `${destination}.tree-sitter-wasms-transaction` + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + const signalState = { requested: undefined } + const filesystem = { + ...fs.promises, + rename(sourcePath, destinationPath) { + if (sourcePath.includes(`${path.sep}backup${path.sep}`)) throw new Error("restore failed") + return fs.promises.rename(sourcePath, destinationPath) + }, + } + + await expect( + publishTreeSitterWasms(source, destination, { + filesystem, + signalState, + onStep(name) { + if (name === "published") signalState.requested = "SIGTERM" + }, + }), + ).rejects.toThrow(`WASM rollback incomplete; recovery retained at ${transaction}`) + expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) + }) }) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 866b23a91e..676a904f77 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -41,6 +41,12 @@ if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitt const dist = path.join(root, "src", "dist") const cacheDir = path.join(root, ".turbo", "coverage-contract") +for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => { + fs.rmSync(cacheDir, { recursive: true, force: true }) + process.exit(1) + }) +} fs.mkdirSync(dist, { recursive: true }) fs.rmSync(cacheDir, { recursive: true, force: true }) for (const filename of fs.readdirSync(dist)) { From ec7fd78089103f3b3ee63f995ff65b92162d1bf4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 02:29:55 +0000 Subject: [PATCH 16/28] fix(ci): preserve WASMs through interruption --- src/scripts/copy-tree-sitter-wasms.mjs | 22 ++++++++---- src/scripts/copy-tree-sitter-wasms.spec.mjs | 39 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index f86bcca2d5..2abe341bde 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -33,15 +33,20 @@ export async function publishTreeSitterWasms( if (signalState.requested) throw Object.assign(new Error("WASM publication cancelled"), { code: "CANCELLED" }) } - await filesystem.mkdir(destinationDir, { recursive: true }) - await filesystem.mkdir(transactionDir) - await filesystem.mkdir(stagedDir) - await filesystem.mkdir(backupDir) - await filesystem.mkdir(quarantineDir) - let commitStarted = false const publishedFiles = [] try { + await filesystem.mkdir(destinationDir, { recursive: true }) + await step("initialized", destinationDir) + await filesystem.mkdir(transactionDir) + await step("initialized", transactionDir) + await filesystem.mkdir(stagedDir) + await step("initialized", stagedDir) + await filesystem.mkdir(backupDir) + await step("initialized", backupDir) + await filesystem.mkdir(quarantineDir) + await step("initialized", quarantineDir) + for (const filename of sourceFiles) { await filesystem.copyFile(path.join(sourceDir, filename), path.join(stagedDir, filename)) await step("staged", filename) @@ -61,8 +66,10 @@ export async function publishTreeSitterWasms( await step("published", filename) } for (const filename of await filesystem.readdir(destinationDir)) { - if (temporaryPattern.test(filename)) + if (temporaryPattern.test(filename)) { await filesystem.rm(path.join(destinationDir, filename), { force: true }) + await step("removed-temporary", filename) + } } await filesystem.rm(transactionDir, { recursive: true, force: true }) @@ -109,6 +116,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { } try { await publishTreeSitterWasms(wasmDir, distDir, { signalState }) + if (signalState.requested) process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 } catch (error) { if (!signalState.requested) throw error process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 38875ed64b..c08cdf3890 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -129,4 +129,43 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) }) + + it("cleans an incomplete transaction setup", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + const transaction = `${destination}.tree-sitter-wasms-transaction` + fs.mkdirSync(source) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + const filesystem = { + ...fs.promises, + mkdir(directory, options) { + if (directory.endsWith(`${path.sep}backup`)) throw new Error("setup failed") + return fs.promises.mkdir(directory, options) + }, + } + + await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("setup failed") + expect(fs.existsSync(transaction)).toBe(false) + }) + + it("observes a signal during temporary cleanup", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm.999.tmp"), "partial") + const signalState = { requested: undefined } + + await expect( + publishTreeSitterWasms(source, destination, { + signalState, + onStep(name) { + if (name === "removed-temporary") signalState.requested = "SIGTERM" + }, + }), + ).rejects.toThrow("WASM publication cancelled") + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + }) }) From 3c6556860019dd8212aada86662d0c44f01bba6b Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 02:47:28 +0000 Subject: [PATCH 17/28] fix(ci): retain interrupted WASM recovery --- src/scripts/copy-tree-sitter-wasms.mjs | 6 ++++-- src/scripts/copy-tree-sitter-wasms.spec.mjs | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 2abe341bde..97a644fb07 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -34,11 +34,13 @@ export async function publishTreeSitterWasms( } let commitStarted = false + let ownsTransaction = false const publishedFiles = [] try { await filesystem.mkdir(destinationDir, { recursive: true }) await step("initialized", destinationDir) await filesystem.mkdir(transactionDir) + ownsTransaction = true await step("initialized", transactionDir) await filesystem.mkdir(stagedDir) await step("initialized", stagedDir) @@ -75,10 +77,10 @@ export async function publishTreeSitterWasms( await filesystem.rm(transactionDir, { recursive: true, force: true }) return { sourceFiles, cleanup: () => cleanPublishedTreeSitterWasms(destinationDir) } } catch (error) { - if (!commitStarted) { + if (!commitStarted && ownsTransaction) { await filesystem.rm(transactionDir, { recursive: true, force: true }) - throw error } + if (!commitStarted) throw error const failures = [] for (const filename of publishedFiles) { diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index c08cdf3890..61f1a6e235 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -128,6 +128,8 @@ describe("publishTreeSitterWasms", () => { ).rejects.toThrow(`WASM rollback incomplete; recovery retained at ${transaction}`) expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) + expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) }) it("cleans an incomplete transaction setup", async () => { From d6d9c21d82aa34e01145bb218f3de75814bc47a3 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:02:15 +0000 Subject: [PATCH 18/28] test(ci): preserve recovery across retries --- src/scripts/copy-tree-sitter-wasms.spec.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 61f1a6e235..c97e16c767 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -129,6 +129,7 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) }) From 437838449ab8f1ce3e639777cd2caef2f645a068 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:03:56 +0000 Subject: [PATCH 19/28] test(ci): remove duplicate recovery assertion --- src/scripts/copy-tree-sitter-wasms.spec.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index c97e16c767..61f1a6e235 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -129,7 +129,6 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) }) From c493be8ec4485223b370d36a6801f01f3ba947fb Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:20:04 +0000 Subject: [PATCH 20/28] fix(ci): protect WASM transaction boundaries --- src/scripts/copy-tree-sitter-wasms.mjs | 4 +++ src/scripts/copy-tree-sitter-wasms.spec.mjs | 33 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 97a644fb07..77d3e02589 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -28,12 +28,14 @@ export async function publishTreeSitterWasms( const backupDir = path.join(transactionDir, "backup") const quarantineDir = path.join(transactionDir, "quarantine") const sourceFiles = (await filesystem.readdir(sourceDir)).filter((filename) => wasmPattern.test(filename)).sort() + if (sourceFiles.length === 0) throw new Error("WASM source set is empty") const step = async (name, filename) => { await onStep(name, filename) if (signalState.requested) throw Object.assign(new Error("WASM publication cancelled"), { code: "CANCELLED" }) } let commitStarted = false + let committed = false let ownsTransaction = false const publishedFiles = [] try { @@ -74,9 +76,11 @@ export async function publishTreeSitterWasms( } } + committed = true await filesystem.rm(transactionDir, { recursive: true, force: true }) return { sourceFiles, cleanup: () => cleanPublishedTreeSitterWasms(destinationDir) } } catch (error) { + if (committed) throw error if (!commitStarted && ownsTransaction) { await filesystem.rm(transactionDir, { recursive: true, force: true }) } diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 61f1a6e235..8a7ebfa516 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -30,6 +30,18 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") }) + it("rejects an empty source set before touching published outputs", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "existing") + + await expect(publishTreeSitterWasms(source, destination)).rejects.toThrow("WASM source set is empty") + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("existing") + expect(fs.existsSync(`${destination}.tree-sitter-wasms-transaction`)).toBe(false) + }) + it("restores published outputs when publication fails", async () => { const source = path.join(root, "source") const destination = path.join(root, "dist") @@ -170,4 +182,25 @@ describe("publishTreeSitterWasms", () => { ).rejects.toThrow("WASM publication cancelled") expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") }) + + it("keeps committed outputs when transaction cleanup fails", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + const transaction = `${destination}.tree-sitter-wasms-transaction` + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + const filesystem = { + ...fs.promises, + rm(directory, options) { + if (directory === transaction) throw new Error("cleanup failed") + return fs.promises.rm(directory, options) + }, + } + + await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("cleanup failed") + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("new-a") + expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + }) }) From 79b0de8d98731498b54ebd9718e9e65ec3b28204 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 12:51:34 +0000 Subject: [PATCH 21/28] fix(ci): preserve WASMs during cache verification --- src/scripts/copy-tree-sitter-wasms.mjs | 4 +- src/scripts/copy-tree-sitter-wasms.spec.mjs | 20 ++++++++ src/scripts/verify-coverage-contract.mjs | 10 ++-- src/scripts/wasm-output-snapshot.mjs | 36 +++++++++++++++ src/scripts/wasm-output-snapshot.spec.mjs | 51 +++++++++++++++++++++ 5 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 src/scripts/wasm-output-snapshot.mjs create mode 100644 src/scripts/wasm-output-snapshot.spec.mjs diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 77d3e02589..9aa211df45 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -69,7 +69,9 @@ export async function publishTreeSitterWasms( publishedFiles.push(filename) await step("published", filename) } - for (const filename of await filesystem.readdir(destinationDir)) { + const destinationFiles = await filesystem.readdir(destinationDir) + await step("inspected-temporaries", destinationDir) + for (const filename of destinationFiles) { if (temporaryPattern.test(filename)) { await filesystem.rm(path.join(destinationDir, filename), { force: true }) await step("removed-temporary", filename) diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 8a7ebfa516..5445f630f9 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -183,6 +183,26 @@ describe("publishTreeSitterWasms", () => { expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") }) + it("observes a signal after inspecting a destination without temporary files", async () => { + const source = path.join(root, "source") + const destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") + const signalState = { requested: undefined } + + await expect( + publishTreeSitterWasms(source, destination, { + signalState, + onStep(name) { + if (name === "inspected-temporaries") signalState.requested = "SIGTERM" + }, + }), + ).rejects.toThrow("WASM publication cancelled") + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + }) + it("keeps committed outputs when transaction cleanup fails", async () => { const source = path.join(root, "source") const destination = path.join(root, "dist") diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 676a904f77..93f4124c4e 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -5,6 +5,7 @@ import process from "node:process" import { fileURLToPath } from "node:url" import { assertMatchingFiles } from "./verify-wasm-files.mjs" +import { createWasmOutputSnapshot } from "./wasm-output-snapshot.mjs" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" @@ -41,17 +42,15 @@ if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitt const dist = path.join(root, "src", "dist") const cacheDir = path.join(root, ".turbo", "coverage-contract") +fs.rmSync(cacheDir, { recursive: true, force: true }) +const outputSnapshot = createWasmOutputSnapshot(dist) for (const signal of ["SIGINT", "SIGTERM"]) { process.once(signal, () => { + outputSnapshot?.restore() fs.rmSync(cacheDir, { recursive: true, force: true }) process.exit(1) }) } -fs.mkdirSync(dist, { recursive: true }) -fs.rmSync(cacheDir, { recursive: true, force: true }) -for (const filename of fs.readdirSync(dist)) { - if (/^tree-sitter-.*\.wasm(?:\.\d+\.tmp)?$/.test(filename)) fs.rmSync(path.join(dist, filename), { force: true }) -} try { run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) @@ -106,5 +105,6 @@ try { "WASM cache restored corrupted output", ) } finally { + outputSnapshot.restore() fs.rmSync(cacheDir, { recursive: true, force: true }) } diff --git a/src/scripts/wasm-output-snapshot.mjs b/src/scripts/wasm-output-snapshot.mjs new file mode 100644 index 0000000000..213af38e01 --- /dev/null +++ b/src/scripts/wasm-output-snapshot.mjs @@ -0,0 +1,36 @@ +import fs from "node:fs" +import path from "node:path" + +const wasmPattern = /^tree-sitter-.*\.wasm(?:\.\d+\.tmp)?$/ + +export function createWasmOutputSnapshot(destinationDir, filesystem = fs) { + const transactionDir = `${destinationDir}.coverage-contract-backup` + const backupDir = path.join(transactionDir, "backup") + const generatedDir = path.join(transactionDir, "generated") + filesystem.mkdirSync(destinationDir, { recursive: true }) + filesystem.mkdirSync(transactionDir) + filesystem.mkdirSync(backupDir) + filesystem.mkdirSync(generatedDir) + + for (const filename of filesystem.readdirSync(destinationDir)) { + if (wasmPattern.test(filename)) + filesystem.renameSync(path.join(destinationDir, filename), path.join(backupDir, filename)) + } + + let restored = false + return { + restore() { + if (restored) return + for (const filename of filesystem.readdirSync(destinationDir)) { + if (wasmPattern.test(filename)) { + filesystem.renameSync(path.join(destinationDir, filename), path.join(generatedDir, filename)) + } + } + for (const filename of filesystem.readdirSync(backupDir)) { + filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) + } + restored = true + filesystem.rmSync(transactionDir, { recursive: true, force: true }) + }, + } +} diff --git a/src/scripts/wasm-output-snapshot.spec.mjs b/src/scripts/wasm-output-snapshot.spec.mjs new file mode 100644 index 0000000000..ae7b4e613e --- /dev/null +++ b/src/scripts/wasm-output-snapshot.spec.mjs @@ -0,0 +1,51 @@ +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { afterEach, beforeEach, describe, expect, it } from "vitest" + +import { createWasmOutputSnapshot } from "./wasm-output-snapshot.mjs" + +describe("createWasmOutputSnapshot", () => { + let root + let destination + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "wasm-output-snapshot-")) + destination = path.join(root, "dist") + fs.mkdirSync(destination) + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous") + }) + + afterEach(() => fs.rmSync(root, { recursive: true, force: true })) + + it.each(["preparation", "dry run", "cache restoration", "signal"])( + "restores prior outputs after %s failure", + () => { + const snapshot = createWasmOutputSnapshot(destination) + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") + fs.writeFileSync(path.join(destination, "tree-sitter-partial.wasm.123.tmp"), "partial") + + snapshot.restore() + + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") + expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) + }, + ) + + it("retains the backup when restoration is interrupted", () => { + const transaction = `${destination}.coverage-contract-backup` + const filesystem = { + ...fs, + renameSync(source, target) { + if (source.includes(`${path.sep}backup${path.sep}`)) throw new Error("restore interrupted") + return fs.renameSync(source, target) + }, + } + const snapshot = createWasmOutputSnapshot(destination, filesystem) + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") + + expect(() => snapshot.restore()).toThrow("restore interrupted") + expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous") + }) +}) From 74c2bbc0ccde9986a3e5fe6a163b9ad606340f9d Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 13:00:02 +0000 Subject: [PATCH 22/28] fix(ci): make WASM snapshot acquisition atomic --- src/scripts/verify-coverage-contract.mjs | 7 ++--- src/scripts/wasm-output-snapshot.mjs | 33 +++++++++++++++++++---- src/scripts/wasm-output-snapshot.spec.mjs | 18 +++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 93f4124c4e..464e487f89 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -43,14 +43,15 @@ if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitt const dist = path.join(root, "src", "dist") const cacheDir = path.join(root, ".turbo", "coverage-contract") fs.rmSync(cacheDir, { recursive: true, force: true }) -const outputSnapshot = createWasmOutputSnapshot(dist) +const state = { outputSnapshot: undefined } for (const signal of ["SIGINT", "SIGTERM"]) { process.once(signal, () => { - outputSnapshot?.restore() + state.outputSnapshot?.restore() fs.rmSync(cacheDir, { recursive: true, force: true }) process.exit(1) }) } +state.outputSnapshot = createWasmOutputSnapshot(dist) try { run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) @@ -105,6 +106,6 @@ try { "WASM cache restored corrupted output", ) } finally { - outputSnapshot.restore() + state.outputSnapshot.restore() fs.rmSync(cacheDir, { recursive: true, force: true }) } diff --git a/src/scripts/wasm-output-snapshot.mjs b/src/scripts/wasm-output-snapshot.mjs index 213af38e01..f27c19b306 100644 --- a/src/scripts/wasm-output-snapshot.mjs +++ b/src/scripts/wasm-output-snapshot.mjs @@ -8,13 +8,36 @@ export function createWasmOutputSnapshot(destinationDir, filesystem = fs) { const backupDir = path.join(transactionDir, "backup") const generatedDir = path.join(transactionDir, "generated") filesystem.mkdirSync(destinationDir, { recursive: true }) - filesystem.mkdirSync(transactionDir) - filesystem.mkdirSync(backupDir) - filesystem.mkdirSync(generatedDir) + let ownsTransaction = false + const movedFiles = [] + try { + filesystem.mkdirSync(transactionDir) + ownsTransaction = true + filesystem.mkdirSync(backupDir) + filesystem.mkdirSync(generatedDir) - for (const filename of filesystem.readdirSync(destinationDir)) { - if (wasmPattern.test(filename)) + for (const filename of filesystem.readdirSync(destinationDir)) { + if (!wasmPattern.test(filename)) continue filesystem.renameSync(path.join(destinationDir, filename), path.join(backupDir, filename)) + movedFiles.push(filename) + } + } catch (error) { + const failures = [] + for (const filename of movedFiles.reverse()) { + try { + filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) + } catch (rollbackError) { + failures.push(rollbackError) + } + } + if (failures.length > 0) { + throw new AggregateError( + [error, ...failures], + `WASM snapshot rollback incomplete; recovery retained at ${transactionDir}`, + ) + } + if (ownsTransaction) filesystem.rmSync(transactionDir, { recursive: true, force: true }) + throw error } let restored = false diff --git a/src/scripts/wasm-output-snapshot.spec.mjs b/src/scripts/wasm-output-snapshot.spec.mjs index ae7b4e613e..fc03cbc8d8 100644 --- a/src/scripts/wasm-output-snapshot.spec.mjs +++ b/src/scripts/wasm-output-snapshot.spec.mjs @@ -48,4 +48,22 @@ describe("createWasmOutputSnapshot", () => { expect(() => snapshot.restore()).toThrow("restore interrupted") expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous") }) + + it("restores files when snapshot creation fails during a later move", () => { + fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") + let moves = 0 + const filesystem = { + ...fs, + renameSync(source, target) { + if (source.startsWith(destination) && ++moves === 2) throw new Error("snapshot failed") + return fs.renameSync(source, target) + }, + } + + expect(() => createWasmOutputSnapshot(destination, filesystem)).toThrow("snapshot failed") + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") + expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") + expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) + }) }) From dc59ffe7a332155f15fca9108c979c52bd50d5d4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 13:04:38 +0000 Subject: [PATCH 23/28] fix(ci): make WASM snapshot restore retryable --- src/scripts/wasm-output-snapshot.mjs | 4 +++- src/scripts/wasm-output-snapshot.spec.mjs | 25 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/scripts/wasm-output-snapshot.mjs b/src/scripts/wasm-output-snapshot.mjs index f27c19b306..2427d16a47 100644 --- a/src/scripts/wasm-output-snapshot.mjs +++ b/src/scripts/wasm-output-snapshot.mjs @@ -41,16 +41,18 @@ export function createWasmOutputSnapshot(destinationDir, filesystem = fs) { } let restored = false + const restoredFiles = new Set() return { restore() { if (restored) return for (const filename of filesystem.readdirSync(destinationDir)) { - if (wasmPattern.test(filename)) { + if (wasmPattern.test(filename) && !restoredFiles.has(filename)) { filesystem.renameSync(path.join(destinationDir, filename), path.join(generatedDir, filename)) } } for (const filename of filesystem.readdirSync(backupDir)) { filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) + restoredFiles.add(filename) } restored = true filesystem.rmSync(transactionDir, { recursive: true, force: true }) diff --git a/src/scripts/wasm-output-snapshot.spec.mjs b/src/scripts/wasm-output-snapshot.spec.mjs index fc03cbc8d8..6cafc7330d 100644 --- a/src/scripts/wasm-output-snapshot.spec.mjs +++ b/src/scripts/wasm-output-snapshot.spec.mjs @@ -66,4 +66,29 @@ describe("createWasmOutputSnapshot", () => { expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) }) + + it("retries a partial restoration without deleting an already restored original", () => { + fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") + let restoreMoves = 0 + let failRestore = true + const filesystem = { + ...fs, + renameSync(source, target) { + if (source.includes(`${path.sep}backup${path.sep}`) && ++restoreMoves === 2 && failRestore) { + failRestore = false + throw new Error("restore interrupted") + } + return fs.renameSync(source, target) + }, + } + const snapshot = createWasmOutputSnapshot(destination, filesystem) + fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") + + expect(() => snapshot.restore()).toThrow("restore interrupted") + expect(() => snapshot.restore()).not.toThrow() + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") + expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") + expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) + }) }) From edb92d0e52803a24f825454a3ec4cf1cf8ce1643 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 13:20:41 +0000 Subject: [PATCH 24/28] fix(ci): separate verifier cache cleanup --- src/scripts/verify-coverage-contract.mjs | 14 ++++++++++---- src/scripts/wasm-output-snapshot.mjs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 464e487f89..5a81f8b15a 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -46,8 +46,11 @@ fs.rmSync(cacheDir, { recursive: true, force: true }) const state = { outputSnapshot: undefined } for (const signal of ["SIGINT", "SIGTERM"]) { process.once(signal, () => { - state.outputSnapshot?.restore() - fs.rmSync(cacheDir, { recursive: true, force: true }) + try { + state.outputSnapshot?.restore() + } finally { + fs.rmSync(cacheDir, { recursive: true, force: true }) + } process.exit(1) }) } @@ -106,6 +109,9 @@ try { "WASM cache restored corrupted output", ) } finally { - state.outputSnapshot.restore() - fs.rmSync(cacheDir, { recursive: true, force: true }) + try { + state.outputSnapshot.restore() + } finally { + fs.rmSync(cacheDir, { recursive: true, force: true }) + } } diff --git a/src/scripts/wasm-output-snapshot.mjs b/src/scripts/wasm-output-snapshot.mjs index 2427d16a47..2e3e8dbfe6 100644 --- a/src/scripts/wasm-output-snapshot.mjs +++ b/src/scripts/wasm-output-snapshot.mjs @@ -50,7 +50,7 @@ export function createWasmOutputSnapshot(destinationDir, filesystem = fs) { filesystem.renameSync(path.join(destinationDir, filename), path.join(generatedDir, filename)) } } - for (const filename of filesystem.readdirSync(backupDir)) { + for (const filename of filesystem.readdirSync(backupDir).sort()) { filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) restoredFiles.add(filename) } From 17aaaef855cc3498df033a811a4e90a7c78e9ed3 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 14:41:33 +0000 Subject: [PATCH 25/28] refactor(ci): simplify generated WASM recovery --- src/scripts/copy-tree-sitter-wasms.mjs | 116 +++--------- src/scripts/copy-tree-sitter-wasms.spec.mjs | 199 +++----------------- src/scripts/dist-sandbox.mjs | 18 ++ src/scripts/dist-sandbox.spec.mjs | 40 ++++ src/scripts/verify-coverage-contract.mjs | 10 +- src/scripts/wasm-output-snapshot.mjs | 61 ------ src/scripts/wasm-output-snapshot.spec.mjs | 94 --------- 7 files changed, 123 insertions(+), 415 deletions(-) create mode 100644 src/scripts/dist-sandbox.mjs create mode 100644 src/scripts/dist-sandbox.spec.mjs delete mode 100644 src/scripts/wasm-output-snapshot.mjs delete mode 100644 src/scripts/wasm-output-snapshot.spec.mjs diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 9aa211df45..9f37196339 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -7,121 +7,65 @@ const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const wasmDir = path.join(srcDir, "node_modules", "tree-sitter-wasms", "out") const distDir = path.join(srcDir, "dist") const wasmPattern = /^tree-sitter-.*\.wasm$/ -const temporaryPattern = /^tree-sitter-.*\.wasm\.\d+\.tmp$/ - -export function cleanPublishedTreeSitterWasms(destinationDir, filesystem = fs) { - if (!filesystem.existsSync(destinationDir)) return - for (const filename of filesystem.readdirSync(destinationDir)) { - if (wasmPattern.test(filename) || temporaryPattern.test(filename)) { - filesystem.rmSync(path.join(destinationDir, filename), { force: true }) - } - } -} export async function publishTreeSitterWasms( sourceDir, destinationDir, { filesystem = fs.promises, onStep = async () => {}, signalState = { requested: undefined } } = {}, ) { - const transactionDir = `${destinationDir}.tree-sitter-wasms-transaction` - const stagedDir = path.join(transactionDir, "staged") - const backupDir = path.join(transactionDir, "backup") - const quarantineDir = path.join(transactionDir, "quarantine") + const stagingDir = `${destinationDir}.tree-sitter-wasms-staging` const sourceFiles = (await filesystem.readdir(sourceDir)).filter((filename) => wasmPattern.test(filename)).sort() if (sourceFiles.length === 0) throw new Error("WASM source set is empty") - const step = async (name, filename) => { + const checkpoint = async (name, filename) => { await onStep(name, filename) - if (signalState.requested) throw Object.assign(new Error("WASM publication cancelled"), { code: "CANCELLED" }) + if (signalState.requested) throw new Error("WASM publication cancelled") } - let commitStarted = false - let committed = false - let ownsTransaction = false - const publishedFiles = [] + await filesystem.rm(stagingDir, { recursive: true, force: true }) + await filesystem.mkdir(stagingDir, { recursive: true }) + await filesystem.mkdir(destinationDir, { recursive: true }) try { - await filesystem.mkdir(destinationDir, { recursive: true }) - await step("initialized", destinationDir) - await filesystem.mkdir(transactionDir) - ownsTransaction = true - await step("initialized", transactionDir) - await filesystem.mkdir(stagedDir) - await step("initialized", stagedDir) - await filesystem.mkdir(backupDir) - await step("initialized", backupDir) - await filesystem.mkdir(quarantineDir) - await step("initialized", quarantineDir) - for (const filename of sourceFiles) { - await filesystem.copyFile(path.join(sourceDir, filename), path.join(stagedDir, filename)) - await step("staged", filename) + await filesystem.copyFile(path.join(sourceDir, filename), path.join(stagingDir, filename)) + await checkpoint("staged", filename) } - - const previousFiles = (await filesystem.readdir(destinationDir)).filter((filename) => - wasmPattern.test(filename), - ) - commitStarted = true - for (const filename of previousFiles) { - await filesystem.rename(path.join(destinationDir, filename), path.join(backupDir, filename)) - await step("backed-up", filename) - } - for (const filename of sourceFiles) { - await filesystem.rename(path.join(stagedDir, filename), path.join(destinationDir, filename)) - publishedFiles.push(filename) - await step("published", filename) + for (const filename of await filesystem.readdir(stagingDir)) { + if (!wasmPattern.test(filename)) throw new Error(`Unexpected staged WASM: ${filename}`) } - const destinationFiles = await filesystem.readdir(destinationDir) - await step("inspected-temporaries", destinationDir) - for (const filename of destinationFiles) { - if (temporaryPattern.test(filename)) { + await checkpoint("validated", stagingDir) + + for (const filename of await filesystem.readdir(destinationDir)) { + if (wasmPattern.test(filename) || (filename.includes("tree-sitter-") && filename.endsWith(".tmp"))) { await filesystem.rm(path.join(destinationDir, filename), { force: true }) - await step("removed-temporary", filename) } } - - committed = true - await filesystem.rm(transactionDir, { recursive: true, force: true }) - return { sourceFiles, cleanup: () => cleanPublishedTreeSitterWasms(destinationDir) } - } catch (error) { - if (committed) throw error - if (!commitStarted && ownsTransaction) { - await filesystem.rm(transactionDir, { recursive: true, force: true }) - } - if (!commitStarted) throw error - - const failures = [] - for (const filename of publishedFiles) { - try { - await filesystem.rename(path.join(destinationDir, filename), path.join(quarantineDir, filename)) - } catch (rollbackError) { - if (rollbackError.code !== "ENOENT") failures.push(rollbackError) - } + for (const filename of sourceFiles) { + await filesystem.rename(path.join(stagingDir, filename), path.join(destinationDir, filename)) + await checkpoint("published", filename) } - for (const filename of await filesystem.readdir(backupDir)) { - try { - await filesystem.rename(path.join(backupDir, filename), path.join(destinationDir, filename)) - await onStep("restored", filename) - } catch (rollbackError) { - failures.push(rollbackError) + await checkpoint("completed", destinationDir) + } catch (error) { + try { + for (const filename of await filesystem.readdir(destinationDir)) { + if (wasmPattern.test(filename)) + await filesystem.rm(path.join(destinationDir, filename), { force: true }) } - } - if (failures.length > 0) { + } catch (cleanupError) { throw new AggregateError( - [error, ...failures], - `WASM rollback incomplete; recovery retained at ${transactionDir}`, + [error, cleanupError], + `WASM publication and cleanup failed; remove ${stagingDir} and rerun to rebuild`, ) } - await filesystem.rm(transactionDir, { recursive: true, force: true }) throw error + } finally { + await filesystem.rm(stagingDir, { recursive: true, force: true }) } + return { sourceFiles } } if (process.argv[1] === fileURLToPath(import.meta.url)) { const signalState = { requested: undefined } - for (const signal of ["SIGINT", "SIGTERM"]) { - process.on(signal, () => { - signalState.requested ??= signal - }) - } + for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => (signalState.requested ??= signal)) try { await publishTreeSitterWasms(wasmDir, distDir, { signalState }) if (signalState.requested) process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index 5445f630f9..dd715e9fef 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -7,220 +7,81 @@ import { publishTreeSitterWasms } from "./copy-tree-sitter-wasms.mjs" describe("publishTreeSitterWasms", () => { let root + let source + let destination beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasms-")) + source = path.join(root, "source") + destination = path.join(root, "dist") + fs.mkdirSync(source) + fs.mkdirSync(destination) + fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") }) afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - it("publishes the exact WASM set and removes stale outputs", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + it("publishes the exact staged set and removes stale state", async () => { fs.writeFileSync(path.join(source, "ignored.txt"), "ignored") fs.writeFileSync(path.join(destination, "tree-sitter-stale.wasm"), "stale") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm.999.tmp"), "partial") + fs.mkdirSync(`${destination}.tree-sitter-wasms-staging`) + fs.writeFileSync(path.join(`${destination}.tree-sitter-wasms-staging`, "stale.tmp"), "stale") await publishTreeSitterWasms(source, destination) expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") + expect(fs.existsSync(`${destination}.tree-sitter-wasms-staging`)).toBe(false) }) - it("rejects an empty source set before touching published outputs", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "existing") - - await expect(publishTreeSitterWasms(source, destination)).rejects.toThrow("WASM source set is empty") - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("existing") - expect(fs.existsSync(`${destination}.tree-sitter-wasms-transaction`)).toBe(false) - }) - - it("restores published outputs when publication fails", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") - fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") - fs.writeFileSync(path.join(destination, "tree-sitter-existing.wasm"), "existing") - let copies = 0 + it("fails without publishing when staging fails", async () => { const filesystem = { ...fs.promises, - copyFile(...args) { - if (++copies === 2) throw new Error("copy failed") - return fs.promises.copyFile(...args) + copyFile: async () => { + throw new Error("copy failed") }, } - await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("copy failed") - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-existing.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-existing.wasm"), "utf8")).toBe("existing") + expect(fs.readdirSync(destination)).toEqual([]) }) - it("restores published outputs when an atomic rename fails", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") + it("removes partial outputs and fails when publication is interrupted", async () => { fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") - fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") - let renames = 0 - const filesystem = { - ...fs.promises, - rename(...args) { - if (args[0].includes(`${path.sep}staged${path.sep}`) && ++renames === 2) - throw new Error("rename failed") - return fs.promises.rename(...args) - }, - } - - await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("rename failed") - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") - }) - - it("restores published outputs when signalled during commit", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") const signalState = { requested: undefined } - await expect( publishTreeSitterWasms(source, destination, { signalState, onStep(name) { if (name === "published") signalState.requested = "SIGTERM" - if (name === "restored") signalState.requested ??= "SIGINT" }, }), ).rejects.toThrow("WASM publication cancelled") - expect(signalState.requested).toBe("SIGTERM") - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - expect(fs.existsSync(`${destination}.tree-sitter-wasms-transaction`)).toBe(false) + expect(fs.readdirSync(destination)).toEqual([]) }) - it("retains the backup when restoration fails", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - const transaction = `${destination}.tree-sitter-wasms-transaction` - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") - const signalState = { requested: undefined } + it("reports publication and cleanup errors together", async () => { + let destinationReads = 0 const filesystem = { ...fs.promises, - rename(sourcePath, destinationPath) { - if (sourcePath.includes(`${path.sep}backup${path.sep}`)) throw new Error("restore failed") - return fs.promises.rename(sourcePath, destinationPath) + readdir(directory) { + if (directory === destination && ++destinationReads === 2) throw new Error("cleanup enumeration failed") + return fs.promises.readdir(directory) }, } await expect( publishTreeSitterWasms(source, destination, { filesystem, - signalState, - onStep(name) { - if (name === "published") signalState.requested = "SIGTERM" - }, - }), - ).rejects.toThrow(`WASM rollback incomplete; recovery retained at ${transaction}`) - expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) - expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - await expect(publishTreeSitterWasms(source, destination)).rejects.toMatchObject({ code: "EEXIST" }) - }) - - it("cleans an incomplete transaction setup", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - const transaction = `${destination}.tree-sitter-wasms-transaction` - fs.mkdirSync(source) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") - const filesystem = { - ...fs.promises, - mkdir(directory, options) { - if (directory.endsWith(`${path.sep}backup`)) throw new Error("setup failed") - return fs.promises.mkdir(directory, options) - }, - } - - await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("setup failed") - expect(fs.existsSync(transaction)).toBe(false) - }) - - it("observes a signal during temporary cleanup", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm.999.tmp"), "partial") - const signalState = { requested: undefined } - - await expect( - publishTreeSitterWasms(source, destination, { - signalState, onStep(name) { - if (name === "removed-temporary") signalState.requested = "SIGTERM" + if (name === "published") throw new Error("publication failed") }, }), - ).rejects.toThrow("WASM publication cancelled") - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - }) - - it("observes a signal after inspecting a destination without temporary files", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") - const signalState = { requested: undefined } - - await expect( - publishTreeSitterWasms(source, destination, { - signalState, - onStep(name) { - if (name === "inspected-temporaries") signalState.requested = "SIGTERM" - }, - }), - ).rejects.toThrow("WASM publication cancelled") - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") - }) - - it("keeps committed outputs when transaction cleanup fails", async () => { - const source = path.join(root, "source") - const destination = path.join(root, "dist") - const transaction = `${destination}.tree-sitter-wasms-transaction` - fs.mkdirSync(source) - fs.mkdirSync(destination) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "new-a") - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous-a") - const filesystem = { - ...fs.promises, - rm(directory, options) { - if (directory === transaction) throw new Error("cleanup failed") - return fs.promises.rm(directory, options) - }, - } - - await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("cleanup failed") - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("new-a") - expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous-a") + ).rejects.toMatchObject({ + message: expect.stringContaining("WASM publication and cleanup failed"), + errors: [ + expect.objectContaining({ message: "publication failed" }), + expect.objectContaining({ message: "cleanup enumeration failed" }), + ], + }) }) }) diff --git a/src/scripts/dist-sandbox.mjs b/src/scripts/dist-sandbox.mjs new file mode 100644 index 0000000000..a2b50b9322 --- /dev/null +++ b/src/scripts/dist-sandbox.mjs @@ -0,0 +1,18 @@ +import fs from "node:fs" + +export function createDistSandbox(distDir, filesystem = fs) { + const backupDir = `${distDir}.coverage-contract-backup` + if (filesystem.existsSync(backupDir)) { + filesystem.rmSync(distDir, { recursive: true, force: true }) + filesystem.renameSync(backupDir, distDir) + } + const hadDist = filesystem.existsSync(distDir) + if (hadDist) filesystem.renameSync(distDir, backupDir) + filesystem.mkdirSync(distDir, { recursive: true }) + return { + restore() { + filesystem.rmSync(distDir, { recursive: true, force: true }) + if (hadDist) filesystem.renameSync(backupDir, distDir) + }, + } +} diff --git a/src/scripts/dist-sandbox.spec.mjs b/src/scripts/dist-sandbox.spec.mjs new file mode 100644 index 0000000000..21f1ee0f79 --- /dev/null +++ b/src/scripts/dist-sandbox.spec.mjs @@ -0,0 +1,40 @@ +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { afterEach, describe, expect, it } from "vitest" + +import { createDistSandbox } from "./dist-sandbox.mjs" + +describe("createDistSandbox", () => { + const roots = [] + afterEach(() => roots.splice(0).forEach((root) => fs.rmSync(root, { recursive: true, force: true }))) + + it.each(["preparation", "dry run", "cache restoration", "signal"])( + "restores developer dist after %s failure", + () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "dist-sandbox-")) + roots.push(root) + const dist = path.join(root, "dist") + fs.mkdirSync(dist) + fs.writeFileSync(path.join(dist, "developer.txt"), "keep") + const sandbox = createDistSandbox(dist) + fs.writeFileSync(path.join(dist, "generated.txt"), "discard") + sandbox.restore() + expect(fs.readdirSync(dist)).toEqual(["developer.txt"]) + expect(fs.readFileSync(path.join(dist, "developer.txt"), "utf8")).toBe("keep") + }, + ) + + it("recovers a stale interrupted sandbox before the next run", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "dist-sandbox-")) + roots.push(root) + const dist = path.join(root, "dist") + fs.mkdirSync(dist) + fs.writeFileSync(path.join(dist, "generated.txt"), "discard") + fs.mkdirSync(`${dist}.coverage-contract-backup`) + fs.writeFileSync(path.join(`${dist}.coverage-contract-backup`, "developer.txt"), "keep") + const sandbox = createDistSandbox(dist) + sandbox.restore() + expect(fs.readdirSync(dist)).toEqual(["developer.txt"]) + }) +}) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 5a81f8b15a..38920755d0 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -5,7 +5,7 @@ import process from "node:process" import { fileURLToPath } from "node:url" import { assertMatchingFiles } from "./verify-wasm-files.mjs" -import { createWasmOutputSnapshot } from "./wasm-output-snapshot.mjs" +import { createDistSandbox } from "./dist-sandbox.mjs" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" @@ -43,18 +43,18 @@ if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitt const dist = path.join(root, "src", "dist") const cacheDir = path.join(root, ".turbo", "coverage-contract") fs.rmSync(cacheDir, { recursive: true, force: true }) -const state = { outputSnapshot: undefined } +const state = { sandbox: undefined } for (const signal of ["SIGINT", "SIGTERM"]) { process.once(signal, () => { try { - state.outputSnapshot?.restore() + state.sandbox?.restore() } finally { fs.rmSync(cacheDir, { recursive: true, force: true }) } process.exit(1) }) } -state.outputSnapshot = createWasmOutputSnapshot(dist) +state.sandbox = createDistSandbox(dist) try { run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) @@ -110,7 +110,7 @@ try { ) } finally { try { - state.outputSnapshot.restore() + state.sandbox.restore() } finally { fs.rmSync(cacheDir, { recursive: true, force: true }) } diff --git a/src/scripts/wasm-output-snapshot.mjs b/src/scripts/wasm-output-snapshot.mjs deleted file mode 100644 index 2e3e8dbfe6..0000000000 --- a/src/scripts/wasm-output-snapshot.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -const wasmPattern = /^tree-sitter-.*\.wasm(?:\.\d+\.tmp)?$/ - -export function createWasmOutputSnapshot(destinationDir, filesystem = fs) { - const transactionDir = `${destinationDir}.coverage-contract-backup` - const backupDir = path.join(transactionDir, "backup") - const generatedDir = path.join(transactionDir, "generated") - filesystem.mkdirSync(destinationDir, { recursive: true }) - let ownsTransaction = false - const movedFiles = [] - try { - filesystem.mkdirSync(transactionDir) - ownsTransaction = true - filesystem.mkdirSync(backupDir) - filesystem.mkdirSync(generatedDir) - - for (const filename of filesystem.readdirSync(destinationDir)) { - if (!wasmPattern.test(filename)) continue - filesystem.renameSync(path.join(destinationDir, filename), path.join(backupDir, filename)) - movedFiles.push(filename) - } - } catch (error) { - const failures = [] - for (const filename of movedFiles.reverse()) { - try { - filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) - } catch (rollbackError) { - failures.push(rollbackError) - } - } - if (failures.length > 0) { - throw new AggregateError( - [error, ...failures], - `WASM snapshot rollback incomplete; recovery retained at ${transactionDir}`, - ) - } - if (ownsTransaction) filesystem.rmSync(transactionDir, { recursive: true, force: true }) - throw error - } - - let restored = false - const restoredFiles = new Set() - return { - restore() { - if (restored) return - for (const filename of filesystem.readdirSync(destinationDir)) { - if (wasmPattern.test(filename) && !restoredFiles.has(filename)) { - filesystem.renameSync(path.join(destinationDir, filename), path.join(generatedDir, filename)) - } - } - for (const filename of filesystem.readdirSync(backupDir).sort()) { - filesystem.renameSync(path.join(backupDir, filename), path.join(destinationDir, filename)) - restoredFiles.add(filename) - } - restored = true - filesystem.rmSync(transactionDir, { recursive: true, force: true }) - }, - } -} diff --git a/src/scripts/wasm-output-snapshot.spec.mjs b/src/scripts/wasm-output-snapshot.spec.mjs deleted file mode 100644 index 6cafc7330d..0000000000 --- a/src/scripts/wasm-output-snapshot.spec.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import fs from "node:fs" -import os from "node:os" -import path from "node:path" -import { afterEach, beforeEach, describe, expect, it } from "vitest" - -import { createWasmOutputSnapshot } from "./wasm-output-snapshot.mjs" - -describe("createWasmOutputSnapshot", () => { - let root - let destination - - beforeEach(() => { - root = fs.mkdtempSync(path.join(os.tmpdir(), "wasm-output-snapshot-")) - destination = path.join(root, "dist") - fs.mkdirSync(destination) - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "previous") - }) - - afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - - it.each(["preparation", "dry run", "cache restoration", "signal"])( - "restores prior outputs after %s failure", - () => { - const snapshot = createWasmOutputSnapshot(destination) - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") - fs.writeFileSync(path.join(destination, "tree-sitter-partial.wasm.123.tmp"), "partial") - - snapshot.restore() - - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") - expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) - }, - ) - - it("retains the backup when restoration is interrupted", () => { - const transaction = `${destination}.coverage-contract-backup` - const filesystem = { - ...fs, - renameSync(source, target) { - if (source.includes(`${path.sep}backup${path.sep}`)) throw new Error("restore interrupted") - return fs.renameSync(source, target) - }, - } - const snapshot = createWasmOutputSnapshot(destination, filesystem) - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") - - expect(() => snapshot.restore()).toThrow("restore interrupted") - expect(fs.readFileSync(path.join(transaction, "backup", "tree-sitter-a.wasm"), "utf8")).toBe("previous") - }) - - it("restores files when snapshot creation fails during a later move", () => { - fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") - let moves = 0 - const filesystem = { - ...fs, - renameSync(source, target) { - if (source.startsWith(destination) && ++moves === 2) throw new Error("snapshot failed") - return fs.renameSync(source, target) - }, - } - - expect(() => createWasmOutputSnapshot(destination, filesystem)).toThrow("snapshot failed") - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") - expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") - expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) - }) - - it("retries a partial restoration without deleting an already restored original", () => { - fs.writeFileSync(path.join(destination, "tree-sitter-b.wasm"), "previous-b") - let restoreMoves = 0 - let failRestore = true - const filesystem = { - ...fs, - renameSync(source, target) { - if (source.includes(`${path.sep}backup${path.sep}`) && ++restoreMoves === 2 && failRestore) { - failRestore = false - throw new Error("restore interrupted") - } - return fs.renameSync(source, target) - }, - } - const snapshot = createWasmOutputSnapshot(destination, filesystem) - fs.writeFileSync(path.join(destination, "tree-sitter-a.wasm"), "generated") - - expect(() => snapshot.restore()).toThrow("restore interrupted") - expect(() => snapshot.restore()).not.toThrow() - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("previous") - expect(fs.readFileSync(path.join(destination, "tree-sitter-b.wasm"), "utf8")).toBe("previous-b") - expect(fs.existsSync(`${destination}.coverage-contract-backup`)).toBe(false) - }) -}) From eaed70a79b0cac67e7160bca225deaebede83193 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 15:55:52 +0000 Subject: [PATCH 26/28] refactor(ci): isolate cached WASM test assets --- .gitignore | 1 + src/scripts/copy-tree-sitter-wasms.mjs | 58 ++------------- src/scripts/copy-tree-sitter-wasms.spec.mjs | 74 ++++++------------- src/scripts/dist-sandbox.mjs | 18 ----- src/scripts/dist-sandbox.spec.mjs | 40 ---------- src/scripts/verify-coverage-contract.mjs | 43 +++-------- src/services/tree-sitter/__tests__/helpers.ts | 8 +- .../__tests__/languageParser.spec.ts | 2 +- src/turbo.json | 8 +- 9 files changed, 50 insertions(+), 202 deletions(-) delete mode 100644 src/scripts/dist-sandbox.mjs delete mode 100644 src/scripts/dist-sandbox.spec.mjs diff --git a/.gitignore b/.gitignore index 3961778d5e..e0ce0c8507 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ mock/ # Builds bin/ *.vsix +/src/generated/ # Local prompts and rules /local-prompts diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs index 9f37196339..84448e6f4b 100644 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ b/src/scripts/copy-tree-sitter-wasms.mjs @@ -5,72 +5,26 @@ import { fileURLToPath } from "node:url" const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const wasmDir = path.join(srcDir, "node_modules", "tree-sitter-wasms", "out") -const distDir = path.join(srcDir, "dist") +const generatedDir = path.join(srcDir, "generated", "tree-sitter-wasms") const wasmPattern = /^tree-sitter-.*\.wasm$/ -export async function publishTreeSitterWasms( - sourceDir, - destinationDir, - { filesystem = fs.promises, onStep = async () => {}, signalState = { requested: undefined } } = {}, -) { - const stagingDir = `${destinationDir}.tree-sitter-wasms-staging` +export async function prepareTreeSitterWasms(sourceDir, destinationDir, { filesystem = fs.promises } = {}) { const sourceFiles = (await filesystem.readdir(sourceDir)).filter((filename) => wasmPattern.test(filename)).sort() if (sourceFiles.length === 0) throw new Error("WASM source set is empty") - const checkpoint = async (name, filename) => { - await onStep(name, filename) - if (signalState.requested) throw new Error("WASM publication cancelled") - } - await filesystem.rm(stagingDir, { recursive: true, force: true }) - await filesystem.mkdir(stagingDir, { recursive: true }) + await filesystem.rm(destinationDir, { recursive: true, force: true }) await filesystem.mkdir(destinationDir, { recursive: true }) try { for (const filename of sourceFiles) { - await filesystem.copyFile(path.join(sourceDir, filename), path.join(stagingDir, filename)) - await checkpoint("staged", filename) - } - for (const filename of await filesystem.readdir(stagingDir)) { - if (!wasmPattern.test(filename)) throw new Error(`Unexpected staged WASM: ${filename}`) - } - await checkpoint("validated", stagingDir) - - for (const filename of await filesystem.readdir(destinationDir)) { - if (wasmPattern.test(filename) || (filename.includes("tree-sitter-") && filename.endsWith(".tmp"))) { - await filesystem.rm(path.join(destinationDir, filename), { force: true }) - } - } - for (const filename of sourceFiles) { - await filesystem.rename(path.join(stagingDir, filename), path.join(destinationDir, filename)) - await checkpoint("published", filename) + await filesystem.copyFile(path.join(sourceDir, filename), path.join(destinationDir, filename)) } - await checkpoint("completed", destinationDir) } catch (error) { - try { - for (const filename of await filesystem.readdir(destinationDir)) { - if (wasmPattern.test(filename)) - await filesystem.rm(path.join(destinationDir, filename), { force: true }) - } - } catch (cleanupError) { - throw new AggregateError( - [error, cleanupError], - `WASM publication and cleanup failed; remove ${stagingDir} and rerun to rebuild`, - ) - } + await filesystem.rm(destinationDir, { recursive: true, force: true }) throw error - } finally { - await filesystem.rm(stagingDir, { recursive: true, force: true }) } return { sourceFiles } } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const signalState = { requested: undefined } - for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => (signalState.requested ??= signal)) - try { - await publishTreeSitterWasms(wasmDir, distDir, { signalState }) - if (signalState.requested) process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 - } catch (error) { - if (!signalState.requested) throw error - process.exitCode = signalState.requested === "SIGINT" ? 130 : 143 - } + await prepareTreeSitterWasms(wasmDir, generatedDir) } diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs index dd715e9fef..663ea023e8 100644 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ b/src/scripts/copy-tree-sitter-wasms.spec.mjs @@ -3,9 +3,9 @@ import os from "node:os" import path from "node:path" import { afterEach, beforeEach, describe, expect, it } from "vitest" -import { publishTreeSitterWasms } from "./copy-tree-sitter-wasms.mjs" +import { prepareTreeSitterWasms } from "./copy-tree-sitter-wasms.mjs" -describe("publishTreeSitterWasms", () => { +describe("prepareTreeSitterWasms", () => { let root let source let destination @@ -13,75 +13,45 @@ describe("publishTreeSitterWasms", () => { beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasms-")) source = path.join(root, "source") - destination = path.join(root, "dist") + destination = path.join(root, "generated", "tree-sitter-wasms") fs.mkdirSync(source) - fs.mkdirSync(destination) fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") }) afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - it("publishes the exact staged set and removes stale state", async () => { + it("replaces pre-existing generated output without filesystem rename", async () => { fs.writeFileSync(path.join(source, "ignored.txt"), "ignored") + fs.mkdirSync(destination, { recursive: true }) fs.writeFileSync(path.join(destination, "tree-sitter-stale.wasm"), "stale") - fs.mkdirSync(`${destination}.tree-sitter-wasms-staging`) - fs.writeFileSync(path.join(`${destination}.tree-sitter-wasms-staging`, "stale.tmp"), "stale") - - await publishTreeSitterWasms(source, destination) - - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") - expect(fs.existsSync(`${destination}.tree-sitter-wasms-staging`)).toBe(false) - }) - - it("fails without publishing when staging fails", async () => { const filesystem = { ...fs.promises, - copyFile: async () => { - throw new Error("copy failed") + rename: async () => { + throw new Error("rename must not be used") }, } - await expect(publishTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("copy failed") - expect(fs.readdirSync(destination)).toEqual([]) - }) - it("removes partial outputs and fails when publication is interrupted", async () => { - fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") - const signalState = { requested: undefined } - await expect( - publishTreeSitterWasms(source, destination, { - signalState, - onStep(name) { - if (name === "published") signalState.requested = "SIGTERM" - }, - }), - ).rejects.toThrow("WASM publication cancelled") - expect(fs.readdirSync(destination)).toEqual([]) + await prepareTreeSitterWasms(source, destination, { filesystem }) + + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) + expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") }) - it("reports publication and cleanup errors together", async () => { - let destinationReads = 0 + it("removes partial task output after a copy failure and rebuilds cleanly", async () => { + fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") + let copies = 0 const filesystem = { ...fs.promises, - readdir(directory) { - if (directory === destination && ++destinationReads === 2) throw new Error("cleanup enumeration failed") - return fs.promises.readdir(directory) + copyFile: async (...args) => { + if (++copies === 2) throw new Error("copy failed") + return fs.promises.copyFile(...args) }, } - await expect( - publishTreeSitterWasms(source, destination, { - filesystem, - onStep(name) { - if (name === "published") throw new Error("publication failed") - }, - }), - ).rejects.toMatchObject({ - message: expect.stringContaining("WASM publication and cleanup failed"), - errors: [ - expect.objectContaining({ message: "publication failed" }), - expect.objectContaining({ message: "cleanup enumeration failed" }), - ], - }) + await expect(prepareTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("copy failed") + expect(fs.existsSync(destination)).toBe(false) + + await prepareTreeSitterWasms(source, destination) + expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) }) }) diff --git a/src/scripts/dist-sandbox.mjs b/src/scripts/dist-sandbox.mjs deleted file mode 100644 index a2b50b9322..0000000000 --- a/src/scripts/dist-sandbox.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import fs from "node:fs" - -export function createDistSandbox(distDir, filesystem = fs) { - const backupDir = `${distDir}.coverage-contract-backup` - if (filesystem.existsSync(backupDir)) { - filesystem.rmSync(distDir, { recursive: true, force: true }) - filesystem.renameSync(backupDir, distDir) - } - const hadDist = filesystem.existsSync(distDir) - if (hadDist) filesystem.renameSync(distDir, backupDir) - filesystem.mkdirSync(distDir, { recursive: true }) - return { - restore() { - filesystem.rmSync(distDir, { recursive: true, force: true }) - if (hadDist) filesystem.renameSync(backupDir, distDir) - }, - } -} diff --git a/src/scripts/dist-sandbox.spec.mjs b/src/scripts/dist-sandbox.spec.mjs deleted file mode 100644 index 21f1ee0f79..0000000000 --- a/src/scripts/dist-sandbox.spec.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import fs from "node:fs" -import os from "node:os" -import path from "node:path" -import { afterEach, describe, expect, it } from "vitest" - -import { createDistSandbox } from "./dist-sandbox.mjs" - -describe("createDistSandbox", () => { - const roots = [] - afterEach(() => roots.splice(0).forEach((root) => fs.rmSync(root, { recursive: true, force: true }))) - - it.each(["preparation", "dry run", "cache restoration", "signal"])( - "restores developer dist after %s failure", - () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "dist-sandbox-")) - roots.push(root) - const dist = path.join(root, "dist") - fs.mkdirSync(dist) - fs.writeFileSync(path.join(dist, "developer.txt"), "keep") - const sandbox = createDistSandbox(dist) - fs.writeFileSync(path.join(dist, "generated.txt"), "discard") - sandbox.restore() - expect(fs.readdirSync(dist)).toEqual(["developer.txt"]) - expect(fs.readFileSync(path.join(dist, "developer.txt"), "utf8")).toBe("keep") - }, - ) - - it("recovers a stale interrupted sandbox before the next run", () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "dist-sandbox-")) - roots.push(root) - const dist = path.join(root, "dist") - fs.mkdirSync(dist) - fs.writeFileSync(path.join(dist, "generated.txt"), "discard") - fs.mkdirSync(`${dist}.coverage-contract-backup`) - fs.writeFileSync(path.join(`${dist}.coverage-contract-backup`, "developer.txt"), "keep") - const sandbox = createDistSandbox(dist) - sandbox.restore() - expect(fs.readdirSync(dist)).toEqual(["developer.txt"]) - }) -}) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 38920755d0..b750e6a0fb 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -5,7 +5,6 @@ import process from "node:process" import { fileURLToPath } from "node:url" import { assertMatchingFiles } from "./verify-wasm-files.mjs" -import { createDistSandbox } from "./dist-sandbox.mjs" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" @@ -37,24 +36,12 @@ const preparationTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#pr if (!coverageTask?.dependencies.includes("zoo-code#prepare:tree-sitter-wasms")) throw new Error("WASM prerequisite missing") if (coverageTask.dependencies.includes("zoo-code#bundle")) throw new Error("Unit coverage must not depend on bundle") -if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["dist/tree-sitter-*.wasm"])) +if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["generated/tree-sitter-wasms/**"])) throw new Error("WASM prerequisite outputs changed") -const dist = path.join(root, "src", "dist") +const generated = path.join(root, "src", "generated", "tree-sitter-wasms") const cacheDir = path.join(root, ".turbo", "coverage-contract") fs.rmSync(cacheDir, { recursive: true, force: true }) -const state = { sandbox: undefined } -for (const signal of ["SIGINT", "SIGTERM"]) { - process.once(signal, () => { - try { - state.sandbox?.restore() - } finally { - fs.rmSync(cacheDir, { recursive: true, force: true }) - } - process.exit(1) - }) -} -state.sandbox = createDistSandbox(dist) try { run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) @@ -64,23 +51,21 @@ try { .readdirSync(path.join(root, "src", "node_modules", "tree-sitter-wasms", "out")) .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) .sort() - const published = fs - .readdirSync(dist) + const prepared = fs + .readdirSync(generated) .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) .sort() if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") - if (JSON.stringify(source) !== JSON.stringify(published)) - throw new Error("Published WASM set does not match dependency") + if (JSON.stringify(source) !== JSON.stringify(prepared)) + throw new Error("Prepared WASM set does not match dependency") assertMatchingFiles( path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), - dist, + generated, source, - "Published WASM content does not match dependency", + "Prepared WASM content does not match dependency", ) - if (fs.readdirSync(dist).some((filename) => filename.endsWith(".tmp"))) - throw new Error("Temporary WASM files remain") - for (const filename of published) fs.rmSync(path.join(dist, filename), { force: true }) + fs.rmSync(generated, { recursive: true, force: true }) const warmGraph = JSON.parse( run( [ @@ -98,20 +83,16 @@ try { if (warmTask?.cache.status !== "HIT") throw new Error("WASM prerequisite is not available in the isolated cache") run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) const restored = fs - .readdirSync(dist) + .readdirSync(generated) .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) .sort() if (JSON.stringify(source) !== JSON.stringify(restored)) throw new Error("WASM cache did not restore exact outputs") assertMatchingFiles( path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), - dist, + generated, source, "WASM cache restored corrupted output", ) } finally { - try { - state.sandbox.restore() - } finally { - fs.rmSync(cacheDir, { recursive: true, force: true }) - } + fs.rmSync(cacheDir, { recursive: true, force: true }) } diff --git a/src/services/tree-sitter/__tests__/helpers.ts b/src/services/tree-sitter/__tests__/helpers.ts index 3f9f4c247c..348cdd1f2d 100644 --- a/src/services/tree-sitter/__tests__/helpers.ts +++ b/src/services/tree-sitter/__tests__/helpers.ts @@ -34,12 +34,12 @@ export async function initializeTreeSitter() { // Initialize directly using the default export or the module itself await Parser.init() - // Override the Parser.Language.load to use dist directory + // Use the cacheable test prerequisite rather than the bundled extension output. const originalLoad = Language.load Language.load = async (wasmPath: string) => { const filename = path.basename(wasmPath) - const correctPath = path.join(process.cwd(), "dist", filename) + const correctPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", filename) // console.log(`Redirecting WASM load from ${wasmPath} to ${correctPath}`) return originalLoad(correctPath) } @@ -84,7 +84,7 @@ export async function testParseSourceCodeDefinitions( const parser = new Parser() // Load language and configure parser - const wasmPath = path.join(process.cwd(), `dist/${wasmFile}`) + const wasmPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", wasmFile) const lang = await Language.load(wasmPath) parser.setLanguage(lang) @@ -113,7 +113,7 @@ export async function testParseSourceCodeDefinitions( export async function inspectTreeStructure(content: string, language: string = "typescript"): Promise { const { Parser, Language } = await initializeTreeSitter() const parser = new Parser() - const wasmPath = path.join(process.cwd(), `dist/tree-sitter-${language}.wasm`) + const wasmPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", `tree-sitter-${language}.wasm`) const lang = await Language.load(wasmPath) parser.setLanguage(lang) diff --git a/src/services/tree-sitter/__tests__/languageParser.spec.ts b/src/services/tree-sitter/__tests__/languageParser.spec.ts index fe4dcdb2d9..e6c254b0d5 100644 --- a/src/services/tree-sitter/__tests__/languageParser.spec.ts +++ b/src/services/tree-sitter/__tests__/languageParser.spec.ts @@ -4,7 +4,7 @@ import * as path from "path" import { loadRequiredLanguageParsers } from "../languageParser" // Path to the directory containing the WASM files. -const WASM_DIR = path.join(__dirname, "../../../node_modules/tree-sitter-wasms/out") +const WASM_DIR = path.join(__dirname, "../../../generated/tree-sitter-wasms") describe("loadRequiredLanguageParsers", () => { it("should load Python parser for .py files", async () => { diff --git a/src/turbo.json b/src/turbo.json index 8ab25798a6..2357ece1ef 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -3,20 +3,20 @@ "extends": ["//"], "tasks": { "test": { - "dependsOn": ["$TURBO_EXTENDS$", "bundle"] + "dependsOn": ["$TURBO_EXTENDS$", "bundle", "prepare:tree-sitter-wasms"] }, "test:unit": { - "dependsOn": ["@roo-code/types#build"], + "dependsOn": ["@roo-code/types#build", "prepare:tree-sitter-wasms"], "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"] }, "test:dist": { "dependsOn": ["bundle"] }, "prepare:tree-sitter-wasms": { - "outputs": ["dist/tree-sitter-*.wasm"] + "outputs": ["generated/tree-sitter-wasms/**"] }, "test:coverage": { - "dependsOn": ["$TURBO_EXTENDS$", "bundle"] + "dependsOn": ["$TURBO_EXTENDS$", "bundle", "prepare:tree-sitter-wasms"] }, "test:coverage:unit": { "dependsOn": ["@roo-code/types#build", "prepare:tree-sitter-wasms"], From a58036a8fcad1385037c951a584ec92e53d30127 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 17:17:33 +0000 Subject: [PATCH 27/28] refactor(ci): load test WASMs from dependency --- .gitignore | 1 - src/package.json | 1 - src/scripts/copy-tree-sitter-wasms.mjs | 30 ----- src/scripts/copy-tree-sitter-wasms.spec.mjs | 57 ---------- src/scripts/verify-coverage-contract.mjs | 107 +++--------------- src/scripts/verify-wasm-files.mjs | 14 --- src/scripts/verify-wasm-files.spec.mjs | 29 ----- src/services/tree-sitter/__tests__/helpers.ts | 18 +-- .../__tests__/languageParser.spec.ts | 2 +- .../tree-sitter/__tests__/wasm.spec.ts | 70 ++++++++++++ src/services/tree-sitter/__tests__/wasm.ts | 14 +++ src/turbo.json | 11 +- 12 files changed, 111 insertions(+), 243 deletions(-) delete mode 100644 src/scripts/copy-tree-sitter-wasms.mjs delete mode 100644 src/scripts/copy-tree-sitter-wasms.spec.mjs delete mode 100644 src/scripts/verify-wasm-files.mjs delete mode 100644 src/scripts/verify-wasm-files.spec.mjs create mode 100644 src/services/tree-sitter/__tests__/wasm.spec.ts create mode 100644 src/services/tree-sitter/__tests__/wasm.ts diff --git a/.gitignore b/.gitignore index e0ce0c8507..3961778d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ mock/ # Builds bin/ *.vsix -/src/generated/ # Local prompts and rules /local-prompts diff --git a/src/package.json b/src/package.json index edf97d6f47..3e873ebbf7 100644 --- a/src/package.json +++ b/src/package.json @@ -442,7 +442,6 @@ "lint": "eslint . --ext=ts --max-warnings=0", "check-types": "tsc --noEmit", "test": "vitest run", - "prepare:tree-sitter-wasms": "node scripts/copy-tree-sitter-wasms.mjs", "verify:coverage-contract": "node scripts/verify-coverage-contract.mjs", "test:unit": "vitest run --config vitest.unit.config.ts", "test:dist": "vitest run --config vitest.dist.config.ts", diff --git a/src/scripts/copy-tree-sitter-wasms.mjs b/src/scripts/copy-tree-sitter-wasms.mjs deleted file mode 100644 index 84448e6f4b..0000000000 --- a/src/scripts/copy-tree-sitter-wasms.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import fs from "node:fs" -import path from "node:path" -import process from "node:process" -import { fileURLToPath } from "node:url" - -const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") -const wasmDir = path.join(srcDir, "node_modules", "tree-sitter-wasms", "out") -const generatedDir = path.join(srcDir, "generated", "tree-sitter-wasms") -const wasmPattern = /^tree-sitter-.*\.wasm$/ - -export async function prepareTreeSitterWasms(sourceDir, destinationDir, { filesystem = fs.promises } = {}) { - const sourceFiles = (await filesystem.readdir(sourceDir)).filter((filename) => wasmPattern.test(filename)).sort() - if (sourceFiles.length === 0) throw new Error("WASM source set is empty") - - await filesystem.rm(destinationDir, { recursive: true, force: true }) - await filesystem.mkdir(destinationDir, { recursive: true }) - try { - for (const filename of sourceFiles) { - await filesystem.copyFile(path.join(sourceDir, filename), path.join(destinationDir, filename)) - } - } catch (error) { - await filesystem.rm(destinationDir, { recursive: true, force: true }) - throw error - } - return { sourceFiles } -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) { - await prepareTreeSitterWasms(wasmDir, generatedDir) -} diff --git a/src/scripts/copy-tree-sitter-wasms.spec.mjs b/src/scripts/copy-tree-sitter-wasms.spec.mjs deleted file mode 100644 index 663ea023e8..0000000000 --- a/src/scripts/copy-tree-sitter-wasms.spec.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import fs from "node:fs" -import os from "node:os" -import path from "node:path" -import { afterEach, beforeEach, describe, expect, it } from "vitest" - -import { prepareTreeSitterWasms } from "./copy-tree-sitter-wasms.mjs" - -describe("prepareTreeSitterWasms", () => { - let root - let source - let destination - - beforeEach(() => { - root = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasms-")) - source = path.join(root, "source") - destination = path.join(root, "generated", "tree-sitter-wasms") - fs.mkdirSync(source) - fs.writeFileSync(path.join(source, "tree-sitter-a.wasm"), "a") - }) - - afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - - it("replaces pre-existing generated output without filesystem rename", async () => { - fs.writeFileSync(path.join(source, "ignored.txt"), "ignored") - fs.mkdirSync(destination, { recursive: true }) - fs.writeFileSync(path.join(destination, "tree-sitter-stale.wasm"), "stale") - const filesystem = { - ...fs.promises, - rename: async () => { - throw new Error("rename must not be used") - }, - } - - await prepareTreeSitterWasms(source, destination, { filesystem }) - - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm"]) - expect(fs.readFileSync(path.join(destination, "tree-sitter-a.wasm"), "utf8")).toBe("a") - }) - - it("removes partial task output after a copy failure and rebuilds cleanly", async () => { - fs.writeFileSync(path.join(source, "tree-sitter-b.wasm"), "b") - let copies = 0 - const filesystem = { - ...fs.promises, - copyFile: async (...args) => { - if (++copies === 2) throw new Error("copy failed") - return fs.promises.copyFile(...args) - }, - } - - await expect(prepareTreeSitterWasms(source, destination, { filesystem })).rejects.toThrow("copy failed") - expect(fs.existsSync(destination)).toBe(false) - - await prepareTreeSitterWasms(source, destination) - expect(fs.readdirSync(destination)).toEqual(["tree-sitter-a.wasm", "tree-sitter-b.wasm"]) - }) -}) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index b750e6a0fb..8675413deb 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -1,98 +1,27 @@ import { spawnSync } from "node:child_process" -import fs from "node:fs" -import path from "node:path" import process from "node:process" -import { fileURLToPath } from "node:url" -import { assertMatchingFiles } from "./verify-wasm-files.mjs" - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" if (!pnpm) throw new Error("pnpm executable path is unavailable") -const pnpmPrefix = process.platform === "win32" ? [pnpm] : [] -const run = (args, options = {}) => { - const { includeStderr = true, ...spawnOptions } = options - const command = process.platform === "win32" ? process.execPath : pnpm - const result = spawnSync(command, [...pnpmPrefix, ...args], { cwd: root, encoding: "utf8", ...spawnOptions }) - if (result.status !== 0) { - const details = [ - result.error?.message, - result.signal ? `terminated by ${result.signal}` : undefined, - result.stderr, - result.stdout, - ] - .filter(Boolean) - .join("\n") - throw new Error(details || `pnpm exited with status ${result.status ?? "unknown"}`) - } - return `${result.stdout || ""}${includeStderr ? result.stderr || "" : ""}` +const command = process.platform === "win32" ? process.execPath : pnpm +const args = process.platform === "win32" ? [pnpm] : [] +const result = spawnSync( + command, + [...args, "turbo", "run", "test:coverage:unit", "test:dist", "--filter=zoo-code", "--dry=json"], + { encoding: "utf8" }, +) +if (result.status !== 0) { + const details = [result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n") + throw new Error(details || `pnpm exited with status ${result.status ?? "unknown"}`) } -const graph = JSON.parse( - run(["turbo", "run", "test:coverage:unit", "--filter=zoo-code", "--dry=json"], { includeStderr: false }), -) +const graph = JSON.parse(result.stdout) const coverageTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:unit") -const preparationTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms") -if (!coverageTask?.dependencies.includes("zoo-code#prepare:tree-sitter-wasms")) - throw new Error("WASM prerequisite missing") +const distTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:dist") +if (!coverageTask) throw new Error("Unit coverage task missing") +if (graph.tasks.some(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms")) + throw new Error("Removed WASM preparation task remains in the graph") if (coverageTask.dependencies.includes("zoo-code#bundle")) throw new Error("Unit coverage must not depend on bundle") -if (JSON.stringify(preparationTask?.outputs) !== JSON.stringify(["generated/tree-sitter-wasms/**"])) - throw new Error("WASM prerequisite outputs changed") - -const generated = path.join(root, "src", "generated", "tree-sitter-wasms") -const cacheDir = path.join(root, ".turbo", "coverage-contract") -fs.rmSync(cacheDir, { recursive: true, force: true }) - -try { - run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) - run(["--dir", "src", "exec", "vitest", "run", "services/tree-sitter/__tests__"], { stdio: "inherit" }) - - const source = fs - .readdirSync(path.join(root, "src", "node_modules", "tree-sitter-wasms", "out")) - .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) - .sort() - const prepared = fs - .readdirSync(generated) - .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) - .sort() - if (source.length === 0) throw new Error("Dependency contains no tree-sitter WASMs") - if (JSON.stringify(source) !== JSON.stringify(prepared)) - throw new Error("Prepared WASM set does not match dependency") - assertMatchingFiles( - path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), - generated, - source, - "Prepared WASM content does not match dependency", - ) - - fs.rmSync(generated, { recursive: true, force: true }) - const warmGraph = JSON.parse( - run( - [ - "turbo", - "run", - "prepare:tree-sitter-wasms", - "--filter=zoo-code", - "--cache-dir=.turbo/coverage-contract", - "--dry=json", - ], - { includeStderr: false }, - ), - ) - const warmTask = warmGraph.tasks.find(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms") - if (warmTask?.cache.status !== "HIT") throw new Error("WASM prerequisite is not available in the isolated cache") - run(["turbo", "run", "prepare:tree-sitter-wasms", "--filter=zoo-code", "--cache-dir=.turbo/coverage-contract"]) - const restored = fs - .readdirSync(generated) - .filter((filename) => /^tree-sitter-.*\.wasm$/.test(filename)) - .sort() - if (JSON.stringify(source) !== JSON.stringify(restored)) throw new Error("WASM cache did not restore exact outputs") - assertMatchingFiles( - path.join(root, "src", "node_modules", "tree-sitter-wasms", "out"), - generated, - source, - "WASM cache restored corrupted output", - ) -} finally { - fs.rmSync(cacheDir, { recursive: true, force: true }) -} +if (!Object.hasOwn(coverageTask.inputs, "package.json")) throw new Error("Unit coverage must hash package.json") +if (!coverageTask.hashOfExternalDependencies) throw new Error("Unit coverage must hash external dependencies") +if (!distTask?.dependencies.includes("zoo-code#bundle")) throw new Error("Dist smoke test must depend on bundle") diff --git a/src/scripts/verify-wasm-files.mjs b/src/scripts/verify-wasm-files.mjs deleted file mode 100644 index 73ef3737da..0000000000 --- a/src/scripts/verify-wasm-files.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -export function assertMatchingFiles(expectedDir, actualDir, filenames, message, filesystem = fs) { - for (const filename of filenames) { - if ( - !filesystem - .readFileSync(path.join(expectedDir, filename)) - .equals(filesystem.readFileSync(path.join(actualDir, filename))) - ) { - throw new Error(`${message}: ${filename}`) - } - } -} diff --git a/src/scripts/verify-wasm-files.spec.mjs b/src/scripts/verify-wasm-files.spec.mjs deleted file mode 100644 index 3beedc642d..0000000000 --- a/src/scripts/verify-wasm-files.spec.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import fs from "node:fs" -import os from "node:os" -import path from "node:path" -import { afterEach, beforeEach, describe, expect, it } from "vitest" - -import { assertMatchingFiles } from "./verify-wasm-files.mjs" - -describe("assertMatchingFiles", () => { - let root - - beforeEach(() => { - root = fs.mkdtempSync(path.join(os.tmpdir(), "verify-wasm-files-")) - }) - - afterEach(() => fs.rmSync(root, { recursive: true, force: true })) - - it("rejects a restored WASM with corrupted content", () => { - const expected = path.join(root, "expected") - const actual = path.join(root, "actual") - fs.mkdirSync(expected) - fs.mkdirSync(actual) - fs.writeFileSync(path.join(expected, "tree-sitter-a.wasm"), "expected") - fs.writeFileSync(path.join(actual, "tree-sitter-a.wasm"), "corrupted") - - expect(() => assertMatchingFiles(expected, actual, ["tree-sitter-a.wasm"], "WASM mismatch")).toThrow( - "WASM mismatch: tree-sitter-a.wasm", - ) - }) -}) diff --git a/src/services/tree-sitter/__tests__/helpers.ts b/src/services/tree-sitter/__tests__/helpers.ts index 348cdd1f2d..904137aaf7 100644 --- a/src/services/tree-sitter/__tests__/helpers.ts +++ b/src/services/tree-sitter/__tests__/helpers.ts @@ -4,6 +4,8 @@ import * as path from "path" import tsxQuery from "../queries/tsx" import { Parser, Language } from "web-tree-sitter" +import { loadTestGrammar } from "./wasm" + vi.mock("fs/promises") export const mockedFs = vi.mocked(fs) @@ -34,16 +36,6 @@ export async function initializeTreeSitter() { // Initialize directly using the default export or the module itself await Parser.init() - // Use the cacheable test prerequisite rather than the bundled extension output. - const originalLoad = Language.load - - Language.load = async (wasmPath: string) => { - const filename = path.basename(wasmPath) - const correctPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", filename) - // console.log(`Redirecting WASM load from ${wasmPath} to ${correctPath}`) - return originalLoad(correctPath) - } - initializedTreeSitter = { Parser, Language } } @@ -84,8 +76,7 @@ export async function testParseSourceCodeDefinitions( const parser = new Parser() // Load language and configure parser - const wasmPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", wasmFile) - const lang = await Language.load(wasmPath) + const lang = await loadTestGrammar(path.basename(wasmFile)) parser.setLanguage(lang) // Create a real query @@ -113,8 +104,7 @@ export async function testParseSourceCodeDefinitions( export async function inspectTreeStructure(content: string, language: string = "typescript"): Promise { const { Parser, Language } = await initializeTreeSitter() const parser = new Parser() - const wasmPath = path.join(process.cwd(), "generated", "tree-sitter-wasms", `tree-sitter-${language}.wasm`) - const lang = await Language.load(wasmPath) + const lang = await loadTestGrammar(`tree-sitter-${language}.wasm`) parser.setLanguage(lang) // Parse the content diff --git a/src/services/tree-sitter/__tests__/languageParser.spec.ts b/src/services/tree-sitter/__tests__/languageParser.spec.ts index e6c254b0d5..fe4dcdb2d9 100644 --- a/src/services/tree-sitter/__tests__/languageParser.spec.ts +++ b/src/services/tree-sitter/__tests__/languageParser.spec.ts @@ -4,7 +4,7 @@ import * as path from "path" import { loadRequiredLanguageParsers } from "../languageParser" // Path to the directory containing the WASM files. -const WASM_DIR = path.join(__dirname, "../../../generated/tree-sitter-wasms") +const WASM_DIR = path.join(__dirname, "../../../node_modules/tree-sitter-wasms/out") describe("loadRequiredLanguageParsers", () => { it("should load Python parser for .py files", async () => { diff --git a/src/services/tree-sitter/__tests__/wasm.spec.ts b/src/services/tree-sitter/__tests__/wasm.spec.ts new file mode 100644 index 0000000000..5b7602cacd --- /dev/null +++ b/src/services/tree-sitter/__tests__/wasm.spec.ts @@ -0,0 +1,70 @@ +import fs from "fs" +import os from "os" +import path from "path" +import { afterEach, beforeAll, describe, expect, it } from "vitest" +import { Parser } from "web-tree-sitter" + +import { loadTestGrammar } from "./wasm" + +const requiredGrammars = [ + "c", + "cpp", + "c_sharp", + "css", + "dart", + "elisp", + "elixir", + "embedded_template", + "go", + "html", + "java", + "javascript", + "json", + "kotlin", + "lua", + "ocaml", + "php", + "python", + "ruby", + "rust", + "scala", + "solidity", + "swift", + "systemrdl", + "tlaplus", + "toml", + "tsx", + "typescript", + "vue", + "zig", +] + +describe("dependency-owned Tree-sitter grammars", () => { + const temporaryDirectories: string[] = [] + + beforeAll(() => Parser.init()) + afterEach(() => temporaryDirectories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true }))) + + it.each(requiredGrammars)("loads tree-sitter-%s.wasm", async (grammar) => { + await expect(loadTestGrammar(`tree-sitter-${grammar}.wasm`)).resolves.toBeDefined() + }) + + it("reports a missing dependency artifact with its filename and resolved path", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasm-missing-")) + temporaryDirectories.push(directory) + + await expect(loadTestGrammar("tree-sitter-missing.wasm", directory)).rejects.toThrow( + `Failed to load Tree-sitter grammar tree-sitter-missing.wasm from ${path.join(directory, "tree-sitter-missing.wasm")}`, + ) + }) + + it("reports a malformed dependency artifact with its filename and resolved path", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasm-malformed-")) + temporaryDirectories.push(directory) + fs.writeFileSync(path.join(directory, "tree-sitter-malformed.wasm"), "not wasm") + + await expect(loadTestGrammar("tree-sitter-malformed.wasm", directory)).rejects.toThrow( + `Failed to load Tree-sitter grammar tree-sitter-malformed.wasm from ${path.join(directory, "tree-sitter-malformed.wasm")}`, + ) + }) +}) diff --git a/src/services/tree-sitter/__tests__/wasm.ts b/src/services/tree-sitter/__tests__/wasm.ts new file mode 100644 index 0000000000..c9dc4d80fc --- /dev/null +++ b/src/services/tree-sitter/__tests__/wasm.ts @@ -0,0 +1,14 @@ +import path from "path" +import { Language } from "web-tree-sitter" + +export const TEST_WASM_DIR = path.join(__dirname, "../../../node_modules/tree-sitter-wasms/out") + +export async function loadTestGrammar(filename: string, directory = TEST_WASM_DIR) { + const wasmPath = path.join(directory, filename) + try { + return await Language.load(wasmPath) + } catch (error) { + const detail = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to load Tree-sitter grammar ${filename} from ${wasmPath}: ${detail}`, { cause: error }) + } +} diff --git a/src/turbo.json b/src/turbo.json index 2357ece1ef..024971987f 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -3,23 +3,20 @@ "extends": ["//"], "tasks": { "test": { - "dependsOn": ["$TURBO_EXTENDS$", "bundle", "prepare:tree-sitter-wasms"] + "dependsOn": ["$TURBO_EXTENDS$", "bundle"] }, "test:unit": { - "dependsOn": ["@roo-code/types#build", "prepare:tree-sitter-wasms"], + "dependsOn": ["@roo-code/types#build"], "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"] }, "test:dist": { "dependsOn": ["bundle"] }, - "prepare:tree-sitter-wasms": { - "outputs": ["generated/tree-sitter-wasms/**"] - }, "test:coverage": { - "dependsOn": ["$TURBO_EXTENDS$", "bundle", "prepare:tree-sitter-wasms"] + "dependsOn": ["$TURBO_EXTENDS$", "bundle"] }, "test:coverage:unit": { - "dependsOn": ["@roo-code/types#build", "prepare:tree-sitter-wasms"], + "dependsOn": ["@roo-code/types#build"], "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"], "outputs": ["coverage/unit/**"] }, From 9e5c92c64612eb033c14d942b8b198f44dbc25d4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 17:41:41 +0000 Subject: [PATCH 28/28] test(ci): strengthen WASM dependency contract --- src/scripts/verify-coverage-contract.mjs | 2 ++ .../tree-sitter/__tests__/wasm.spec.ts | 23 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 8675413deb..1f6003dff1 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -22,6 +22,8 @@ if (!coverageTask) throw new Error("Unit coverage task missing") if (graph.tasks.some(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms")) throw new Error("Removed WASM preparation task remains in the graph") if (coverageTask.dependencies.includes("zoo-code#bundle")) throw new Error("Unit coverage must not depend on bundle") +if (!coverageTask.dependencies.includes("@roo-code/types#build")) + throw new Error("Unit coverage must depend on the types build") if (!Object.hasOwn(coverageTask.inputs, "package.json")) throw new Error("Unit coverage must hash package.json") if (!coverageTask.hashOfExternalDependencies) throw new Error("Unit coverage must hash external dependencies") if (!distTask?.dependencies.includes("zoo-code#bundle")) throw new Error("Dist smoke test must depend on bundle") diff --git a/src/services/tree-sitter/__tests__/wasm.spec.ts b/src/services/tree-sitter/__tests__/wasm.spec.ts index 5b7602cacd..c3114ff164 100644 --- a/src/services/tree-sitter/__tests__/wasm.spec.ts +++ b/src/services/tree-sitter/__tests__/wasm.spec.ts @@ -39,6 +39,20 @@ const requiredGrammars = [ "zig", ] +async function captureLoadFailure(filename: string, directory: string) { + let error: unknown + try { + await loadTestGrammar(filename, directory) + } catch (caught) { + error = caught + } + if (!(error instanceof Error) || !(error.cause instanceof Error)) { + throw new Error("Expected a contextual grammar load error with an Error cause") + } + expect(error.message).toContain(error.cause.message) + return error +} + describe("dependency-owned Tree-sitter grammars", () => { const temporaryDirectories: string[] = [] @@ -46,14 +60,16 @@ describe("dependency-owned Tree-sitter grammars", () => { afterEach(() => temporaryDirectories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true }))) it.each(requiredGrammars)("loads tree-sitter-%s.wasm", async (grammar) => { - await expect(loadTestGrammar(`tree-sitter-${grammar}.wasm`)).resolves.toBeDefined() + const language = await loadTestGrammar(`tree-sitter-${grammar}.wasm`) + expect(() => new Parser().setLanguage(language)).not.toThrow() }) it("reports a missing dependency artifact with its filename and resolved path", async () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tree-sitter-wasm-missing-")) temporaryDirectories.push(directory) - await expect(loadTestGrammar("tree-sitter-missing.wasm", directory)).rejects.toThrow( + const error = await captureLoadFailure("tree-sitter-missing.wasm", directory) + expect(error.message).toContain( `Failed to load Tree-sitter grammar tree-sitter-missing.wasm from ${path.join(directory, "tree-sitter-missing.wasm")}`, ) }) @@ -63,7 +79,8 @@ describe("dependency-owned Tree-sitter grammars", () => { temporaryDirectories.push(directory) fs.writeFileSync(path.join(directory, "tree-sitter-malformed.wasm"), "not wasm") - await expect(loadTestGrammar("tree-sitter-malformed.wasm", directory)).rejects.toThrow( + const error = await captureLoadFailure("tree-sitter-malformed.wasm", directory) + expect(error.message).toContain( `Failed to load Tree-sitter grammar tree-sitter-malformed.wasm from ${path.join(directory, "tree-sitter-malformed.wasm")}`, ) })