From f11e7300676e0e72f264a409360b67097c22f49d Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 05:47:39 +0200 Subject: [PATCH 01/10] test(cli): run the Biome guards on Windows `biome-guards-bite.arch.test.ts` spawns `node_modules/.bin/biome` directly. Windows reaches Biome through a `.cmd` shim, so the launch reportedly fails with ENOENT (#881), and the Windows job never runs the architecture suite, so nothing here has shown it. The Windows job now runs that one file. Refs #881 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .github/workflows/cli-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index bb7dea8a8..69247e546 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -560,6 +560,8 @@ jobs: grep -E "tool files readable +ok" telemetry-check.out - run: cd cli && pnpm test:unit - run: cd cli && pnpm test:integration + - name: Biome guards launch Biome on Windows (#881) + run: cd cli && pnpm exec vitest run --project=architecture tests/architecture/biome-guards-bite.arch.test.ts # Every e2e but the two named below. `telemetry-commit-trailer.e2e.test.ts` is why the # step matters most: it is the only thing proving the `prepare-commit-msg` hook is # reachable by the shell Git for Windows ships, which a backslash path is not. From 320c8d29f84ff6de0be0ae7187a31e7e836f63d3 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 05:55:23 +0200 Subject: [PATCH 02/10] fix(cli): launch Biome through node in the architecture guards On the Windows runner the Biome guards failed with `expected [] to include 'src/kernel/exit.ts:plugin'` and two more like it: spawning `node_modules/.bin/biome` directly found no executable there, and the empty output read as a tree with no findings. The guards now run the package's own launcher, `@biomejs/biome/bin/biome`, through `process.execPath`, which works on every platform. A run that is neither clean (exit 0) nor diagnosed (exit 1 with a finding) throws instead of returning nothing, so a guard asserting that no finding exists can no longer pass when Biome never ran. A test with a missing launcher was red before this. Closes #881 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .../biome-guards-bite.arch.test.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cli/tests/architecture/biome-guards-bite.arch.test.ts b/cli/tests/architecture/biome-guards-bite.arch.test.ts index 25fb04dbe..3704cefee 100644 --- a/cli/tests/architecture/biome-guards-bite.arch.test.ts +++ b/cli/tests/architecture/biome-guards-bite.arch.test.ts @@ -10,21 +10,27 @@ import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { CLI_ROOT, read } from "./helpers.js"; -const BIOME = join(CLI_ROOT, "node_modules", ".bin", "biome"); +// The package's own node launcher: `.bin/biome` is a shell shim, a `.cmd` on Windows. +const BIOME = join(CLI_ROOT, "node_modules", "@biomejs", "biome", "bin", "biome"); const PLUGIN = "biome-plugins/no-process-exit.grit"; /** The diagnostics biome prints for one planted tree, as `:` lines. */ -function biomeFindings(projectDir: string): string[] { - const result = spawnSync(BIOME, ["lint", "--reporter=github", "."], { +function biomeFindings(projectDir: string, launcher = BIOME): string[] { + const result = spawnSync(process.execPath, [launcher, "lint", "--reporter=github", "."], { cwd: projectDir, encoding: "utf8", }); - const lines = `${result.stdout}\n${result.stderr}`.split("\n"); - return lines + const findings = `${result.stdout}\n${result.stderr}` + .split("\n") .map((line) => /^\s*::(?:error|warning) title=(\S+),file=(\S+?),/.exec(line)) .filter((match): match is RegExpExecArray => match !== null) .map((match) => `${match[2]}:${match[1]}`) .sort(); + // biome exits 0 when clean and 1 with diagnostics; anything else never linted. + if (result.status !== 0 && !(result.status === 1 && findings.length > 0)) { + throw new Error(`biome did not run: ${result.error?.message ?? result.stderr}`); + } + return findings; } describe("biome carries the exit and default-export rules", () => { @@ -126,6 +132,12 @@ describe("the guard itself", () => { ); }); + it("fails when biome does not run, rather than reading as no findings", () => { + expect(() => biomeFindings(planted, join(planted, "missing-biome"))).toThrow( + /biome did not run/ + ); + }); + it("flags a default export under src/ and tests/ alike, and a clean module not at all", () => { const findings = biomeFindings(planted); expect(findings).toContain("src/kernel/default.ts:lint/style/noDefaultExport"); From 2da450ef07e402000c6cbc7143254b44479f2612 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 06:23:52 +0200 Subject: [PATCH 03/10] fix(cli): read Biome's Windows paths in the architecture guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With Biome launched, the Windows guards still failed with `expected [ …(5) ] to include 'src/kernel/exit.ts:plugin'`. Biome found the five planted findings it finds elsewhere, but names a file with backslashes there, so no key matched. The parser now turns the separators into forward slashes, under a test fed a Windows line. Refs #881 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .../biome-guards-bite.arch.test.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cli/tests/architecture/biome-guards-bite.arch.test.ts b/cli/tests/architecture/biome-guards-bite.arch.test.ts index 3704cefee..5c820e420 100644 --- a/cli/tests/architecture/biome-guards-bite.arch.test.ts +++ b/cli/tests/architecture/biome-guards-bite.arch.test.ts @@ -14,18 +14,23 @@ import { CLI_ROOT, read } from "./helpers.js"; const BIOME = join(CLI_ROOT, "node_modules", "@biomejs", "biome", "bin", "biome"); const PLUGIN = "biome-plugins/no-process-exit.grit"; -/** The diagnostics biome prints for one planted tree, as `:` lines. */ +/** Biome's GitHub-reporter lines as `:`, the file with forward slashes. */ +function findingsIn(output: string): string[] { + return output + .split("\n") + .map((line) => /^\s*::(?:error|warning) title=(\S+),file=(\S+?),/.exec(line)) + .filter((match): match is RegExpExecArray => match !== null) + .map((match) => `${(match[2] as string).replaceAll("\\", "/")}:${match[1]}`) + .sort(); +} + +/** The diagnostics biome prints for one planted tree. */ function biomeFindings(projectDir: string, launcher = BIOME): string[] { const result = spawnSync(process.execPath, [launcher, "lint", "--reporter=github", "."], { cwd: projectDir, encoding: "utf8", }); - const findings = `${result.stdout}\n${result.stderr}` - .split("\n") - .map((line) => /^\s*::(?:error|warning) title=(\S+),file=(\S+?),/.exec(line)) - .filter((match): match is RegExpExecArray => match !== null) - .map((match) => `${match[2]}:${match[1]}`) - .sort(); + const findings = findingsIn(`${result.stdout}\n${result.stderr}`); // biome exits 0 when clean and 1 with diagnostics; anything else never linted. if (result.status !== 0 && !(result.status === 1 && findings.length > 0)) { throw new Error(`biome did not run: ${result.error?.message ?? result.stderr}`); @@ -132,6 +137,14 @@ describe("the guard itself", () => { ); }); + it("reads a Windows path the way it reads a POSIX one", () => { + expect( + findingsIn( + "::error title=lint/style/noDefaultExport,file=src\\kernel\\default.ts,line=2,endLine=2,col=8,endColumn=15::Avoid default exports." + ) + ).toEqual(["src/kernel/default.ts:lint/style/noDefaultExport"]); + }); + it("fails when biome does not run, rather than reading as no findings", () => { expect(() => biomeFindings(planted, join(planted, "missing-biome"))).toThrow( /biome did not run/ From 1e21bbc8d513c424bd27c8d5493dd7e4f5ecd784 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 06:51:06 +0200 Subject: [PATCH 04/10] chore(ci): name the Windows hook steps by what they do A CI step name, an error message and a test comment carried the number of the issue that led to them. Each now says what it checks, and the issue numbers stay in the commits and pull requests that fixed them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .github/workflows/cli-ci.yml | 6 +++--- .../__tests__/lefthook-runs-hold-no-double-quote.test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 69247e546..ede82d812 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -520,7 +520,7 @@ jobs: - name: Plugin suite run: node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js' - # A Windows commit's hooks (#948), less the three cli jobs that need cli/'s dependencies. + # A Windows commit's hooks, less the three cli jobs that need cli/'s dependencies. - name: Hooks - a commit's hooks pass on Windows run: | echo "# lefthook subject" > "$RUNNER_TEMP/msg" @@ -530,7 +530,7 @@ jobs: pnpm exec lefthook run prepare-commit-msg "$RUNNER_TEMP/msg" message --no-auto-install --no-tty >> "$RUNNER_TEMP/hooks.log" 2>&1 || status=1 cat "$RUNNER_TEMP/hooks.log" if grep -n "syntax error" "$RUNNER_TEMP/hooks.log"; then - echo "::error::lefthook handed sh a broken command line (#948)" + echo "::error::lefthook handed sh a broken command line" exit 1 fi exit $status @@ -560,7 +560,7 @@ jobs: grep -E "tool files readable +ok" telemetry-check.out - run: cd cli && pnpm test:unit - run: cd cli && pnpm test:integration - - name: Biome guards launch Biome on Windows (#881) + - name: Biome guards run on Windows run: cd cli && pnpm exec vitest run --project=architecture tests/architecture/biome-guards-bite.arch.test.ts # Every e2e but the two named below. `telemetry-commit-trailer.e2e.test.ts` is why the # step matters most: it is the only thing proving the `prepare-commit-msg` hook is diff --git a/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js b/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js index 4717bcf30..34973143d 100644 --- a/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js +++ b/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js @@ -6,7 +6,7 @@ const yaml = require("js-yaml"); const root = path.resolve(__dirname, "../.."); -// Lefthook on Windows passes a run to `sh -c` unescaped, so a `"` truncates it (#948). +// Lefthook on Windows passes a run to `sh -c` unescaped, so a `"` truncates it. test("no run in lefthook.yml holds a double quote", () => { const config = yaml.load(fs.readFileSync(path.join(root, "lefthook.yml"), "utf8")); const offenders = []; From dcdcd69a94cbbbc38d72f95ea1bb648a232cd97e Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 06:52:43 +0200 Subject: [PATCH 05/10] docs(framework): keep tracker IDs out of every file, not only docs The documentation rule already dropped ticket and run IDs, but only for the Markdown its paths name, so code, comments, test names and CI steps carried them. A rule with no paths now covers every file: tracker IDs live in commits, pull requests and issues, and the thing is named by what it does. The documentation rule no longer repeats the clause. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .claude/rules/01-standards/1-documentation.md | 2 +- .claude/rules/01-standards/2-tracker-ids.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .claude/rules/01-standards/2-tracker-ids.md diff --git a/.claude/rules/01-standards/1-documentation.md b/.claude/rules/01-standards/1-documentation.md index d47157fb9..fe4cae2ac 100644 --- a/.claude/rules/01-standards/1-documentation.md +++ b/.claude/rules/01-standards/1-documentation.md @@ -14,7 +14,7 @@ paths: - Architecture: components, boundaries, flows, invariants. README: purpose, usage, limits. - Use descriptive, hierarchical headings; include only relevant facts, once per document. - Preserve contracts, decisions, rationale, limits, procedures, linked anchors and generator markers. Keep memory safeguards self-contained; never invent performance requirements. -- Remove filler, repetition, narrated history, dates, counters, ticket/run IDs, snapshots, temporary evidence and measured timings, ratios or comparisons. +- Remove filler, repetition, narrated history, dates, counters, snapshots, temporary evidence and measured timings, ratios or comparisons. - Prefer diagrams for relationships and tables for roles; keep necessary prose concise. Retain required versions, constants and useful examples. - State actions and verification commands; link canonical sources for supporting details. - Read back: check section scope, preserved information, commands and links. diff --git a/.claude/rules/01-standards/2-tracker-ids.md b/.claude/rules/01-standards/2-tracker-ids.md new file mode 100644 index 000000000..090c57337 --- /dev/null +++ b/.claude/rules/01-standards/2-tracker-ids.md @@ -0,0 +1,7 @@ +# Tracker IDs + +Ticket, issue, pull-request and run IDs stay out of code, comments, test names, CI steps and durable docs. + +- Never write a tracker ID there. +- Commits, pull requests and issues carry them. +- Name what the thing does instead. From 0cb9e7a266ec937ab4723784159d3acc9f3c5165 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 06:57:27 +0200 Subject: [PATCH 06/10] chore(ci): run the whole architecture suite on Windows The Windows job ran one file of the architecture suite, the Biome guards. It now runs `pnpm test:arch`, the same suite the Linux architecture job runs, so every architecture guard is exercised on Windows, the Biome guards among them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .github/workflows/cli-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index ede82d812..97c6f9f9e 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -560,8 +560,7 @@ jobs: grep -E "tool files readable +ok" telemetry-check.out - run: cd cli && pnpm test:unit - run: cd cli && pnpm test:integration - - name: Biome guards run on Windows - run: cd cli && pnpm exec vitest run --project=architecture tests/architecture/biome-guards-bite.arch.test.ts + - run: cd cli && pnpm test:arch # Every e2e but the two named below. `telemetry-commit-trailer.e2e.test.ts` is why the # step matters most: it is the only thing proving the `prepare-commit-msg` hook is # reachable by the shell Git for Windows ships, which a backslash path is not. From 286972d01326764515c2b5a0b87c822427898874 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 07:03:54 +0200 Subject: [PATCH 07/10] docs(framework): reduce the tracker-ID rule to one bullet The rule now reads as a single bullet, and the documentation rule keeps its own clause unchanged: its paths only reach the documentation, so the two do not overlap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .claude/rules/01-standards/1-documentation.md | 2 +- .claude/rules/01-standards/2-tracker-ids.md | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.claude/rules/01-standards/1-documentation.md b/.claude/rules/01-standards/1-documentation.md index fe4cae2ac..d47157fb9 100644 --- a/.claude/rules/01-standards/1-documentation.md +++ b/.claude/rules/01-standards/1-documentation.md @@ -14,7 +14,7 @@ paths: - Architecture: components, boundaries, flows, invariants. README: purpose, usage, limits. - Use descriptive, hierarchical headings; include only relevant facts, once per document. - Preserve contracts, decisions, rationale, limits, procedures, linked anchors and generator markers. Keep memory safeguards self-contained; never invent performance requirements. -- Remove filler, repetition, narrated history, dates, counters, snapshots, temporary evidence and measured timings, ratios or comparisons. +- Remove filler, repetition, narrated history, dates, counters, ticket/run IDs, snapshots, temporary evidence and measured timings, ratios or comparisons. - Prefer diagrams for relationships and tables for roles; keep necessary prose concise. Retain required versions, constants and useful examples. - State actions and verification commands; link canonical sources for supporting details. - Read back: check section scope, preserved information, commands and links. diff --git a/.claude/rules/01-standards/2-tracker-ids.md b/.claude/rules/01-standards/2-tracker-ids.md index 090c57337..231be1310 100644 --- a/.claude/rules/01-standards/2-tracker-ids.md +++ b/.claude/rules/01-standards/2-tracker-ids.md @@ -1,7 +1,3 @@ # Tracker IDs -Ticket, issue, pull-request and run IDs stay out of code, comments, test names, CI steps and durable docs. - -- Never write a tracker ID there. -- Commits, pull requests and issues carry them. -- Name what the thing does instead. +- No ticket/run IDs in code, comments, tests or CI. From b70dca2e198d6bf25afbff8f5864a9b4b08c099f Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 08:26:08 +0200 Subject: [PATCH 08/10] chore(framework): check out text files with LF on every platform Run on Windows, the architecture suite failed in three files that read text by its lines: `codebase-map.md has no fenced block`, `expected [] to include 'knip'`, and a SyntaxError loading `mutation-covers-source`. A Windows checkout turns LF into CRLF, which the repository's scripts and tests do not expect. `.gitattributes` now asks for LF on every platform, `text=auto` leaving binary files alone. No committed file changes: the only CRLF files are archived evidence whose own `.gitattributes` already mark them `-text`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf From 0bc58cf70a6cbbcf121a9d6200a02854551da0af Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 09:24:25 +0200 Subject: [PATCH 09/10] fix(cli): resolve both sides of a Biome finding's path in the guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the merge with #912, the Windows guards still failed with `expected [ …(5) ] to include 'src/kernel/exit.ts:plugin'`: Biome found the five planted findings, and none of their paths matched. Only the planted tree went through realpathSync, so a file Biome named through another spelling of the same directory could not be made relative to it. Each file now resolves through realpathSync too, and a failed match prints the findings it saw. Refs #881 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .../biome-guards-bite.arch.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cli/tests/architecture/biome-guards-bite.arch.test.ts b/cli/tests/architecture/biome-guards-bite.arch.test.ts index 989ecf1de..364784d43 100644 --- a/cli/tests/architecture/biome-guards-bite.arch.test.ts +++ b/cli/tests/architecture/biome-guards-bite.arch.test.ts @@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process"; import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, relative, sep } from "node:path"; +import { join, relative, resolve, sep } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { CLI_ROOT, read } from "./helpers.js"; @@ -25,7 +25,10 @@ function biomeFindings(projectDir: string, launcher = BIOME): string[] { .map((line) => /^\s*::(?:error|warning) title=(\S+),file=(\S+?),/.exec(line)) .filter((match): match is RegExpExecArray => match !== null) .map( - (match) => `${relative(realpathSync(projectDir), match[2]).split(sep).join("/")}:${match[1]}` + (match) => + `${relative(realpathSync(projectDir), realpathSync(resolve(projectDir, match[2] as string))) + .split(sep) + .join("/")}:${match[1]}` ) .sort(); // biome exits 0 when clean and 1 with diagnostics; anything else never linted. @@ -116,17 +119,17 @@ describe("the guard itself", () => { it("flags process.exit under kernel/, contexts/ and runtime/, and leaves the command edge alone", () => { const findings = biomeFindings(planted); - expect(findings).toContain("src/kernel/exit.ts:plugin"); + expect(findings, findings.join(", ")).toContain("src/kernel/exit.ts:plugin"); expect(findings.filter((f) => f.startsWith("src/presentation/"))).toEqual([]); }); it("flags the process global in a context's domain and application, and not in its infrastructure", () => { const findings = biomeFindings(planted); - expect(findings).toContain( + expect(findings, findings.join(", ")).toContain( "src/contexts/framework/domain/env.ts:lint/style/noRestrictedGlobals" ); - expect(findings).toContain( + expect(findings, findings.join(", ")).toContain( "src/contexts/framework/application/env.ts:lint/style/noRestrictedGlobals" ); expect(findings.filter((f) => f.startsWith("src/contexts/framework/infrastructure/"))).toEqual( @@ -142,8 +145,12 @@ describe("the guard itself", () => { it("flags a default export under src/ and tests/ alike, and a clean module not at all", () => { const findings = biomeFindings(planted); - expect(findings).toContain("src/kernel/default.ts:lint/style/noDefaultExport"); - expect(findings).toContain("tests/kernel/default.ts:lint/style/noDefaultExport"); + expect(findings, findings.join(", ")).toContain( + "src/kernel/default.ts:lint/style/noDefaultExport" + ); + expect(findings, findings.join(", ")).toContain( + "tests/kernel/default.ts:lint/style/noDefaultExport" + ); expect(findings.filter((f) => f.startsWith("src/kernel/clean.ts"))).toEqual([]); }); }); From 67e0b0f6ad78e95e2f9c7c2e41df26b25ebc0bdc Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Mon, 12 Oct 2026 09:32:26 +0200 Subject: [PATCH 10/10] fix(cli): decode the file the Biome GitHub reporter escapes With the findings now printed, the Windows guards failed on `ENOENT ... lstat '...\aidd-biome-bite-BHHWDT\C%3A'`. On Windows Biome reports an absolute file, and the GitHub reporter escapes its drive colon as `%3A`, so the path resolved under the planted tree instead of to the file. The guard decodes the reporter's `%3A`, `%2C` and `%25` before resolving the path, under a test that was red first. Refs #881 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE16AxC1V9wAMGmGgefe4z --- .../architecture/biome-guards-bite.arch.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cli/tests/architecture/biome-guards-bite.arch.test.ts b/cli/tests/architecture/biome-guards-bite.arch.test.ts index 364784d43..580b3c04a 100644 --- a/cli/tests/architecture/biome-guards-bite.arch.test.ts +++ b/cli/tests/architecture/biome-guards-bite.arch.test.ts @@ -14,6 +14,11 @@ import { CLI_ROOT, read } from "./helpers.js"; const BIOME = join(CLI_ROOT, "node_modules", "@biomejs", "biome", "bin", "biome"); const PLUGIN = "biome-plugins/no-process-exit.grit"; +/** A file property as the GitHub reporter escapes it, `C:` written `C%3A` on Windows. */ +function reportedFile(raw: string): string { + return raw.replace(/%3A/gi, ":").replace(/%2C/gi, ",").replace(/%25/g, "%"); +} + /** The diagnostics biome prints for one planted tree, as `:` lines. */ function biomeFindings(projectDir: string, launcher = BIOME): string[] { const result = spawnSync(process.execPath, [launcher, "lint", "--reporter=github", "."], { @@ -26,7 +31,10 @@ function biomeFindings(projectDir: string, launcher = BIOME): string[] { .filter((match): match is RegExpExecArray => match !== null) .map( (match) => - `${relative(realpathSync(projectDir), realpathSync(resolve(projectDir, match[2] as string))) + `${relative( + realpathSync(projectDir), + realpathSync(resolve(projectDir, reportedFile(match[2] as string))) + ) .split(sep) .join("/")}:${match[1]}` ) @@ -137,6 +145,10 @@ describe("the guard itself", () => { ); }); + it("reads a file the GitHub reporter escaped, as on Windows", () => { + expect(reportedFile("C%3A\\tmp\\a%2Cb%25.ts")).toBe("C:\\tmp\\a,b%.ts"); + }); + it("fails when biome does not run, rather than reading as no findings", () => { expect(() => biomeFindings(planted, join(planted, "missing-biome"))).toThrow( /biome did not run/