Skip to content

Commit cd30888

Browse files
committed
fix(test): canonicalize temp paths so imagine dryRun perFile passes on macOS (symlinked tmpdir)
dryRun() attributes test failures to files by string-matching node --test's `location:` diagnostic (a canonical realpath) against join(wt, testfile). On macOS the sandbox worktree lives under tmpdir(), which is itself a symlink (/var → /private/var, /tmp → /private/tmp), so the raw wt path never matched the realpath'd locations: attribution failed, perFile was omitted, and test/imagine.test.js "dryRun executes the suite in an ephemeral worktree and removes it (pass + fail)" saw perFile === undefined instead of the expected { "test/pass.test.js": "pass", "test/fail.test.js": "fail" }. Fix the code (not the test): realpathSync(wt) once after the worktree is created and compare against that canonical root. On Linux (no symlinked tmpdir) this is a no-op. Re-enable the full unit suite on macos-latest in smoke.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
1 parent 4348e51 commit cd30888

3 files changed

Lines changed: 22 additions & 10 deletions

File tree

.github/workflows/smoke.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,9 @@ jobs:
4242
- name: Install/uninstall smoke (isolated HOME)
4343
shell: bash
4444
run: bash scripts/smoke-install.sh
45-
# Re-run the unit suite here only on Linux — ci.yml already runs it on the Node 20/22
46-
# Linux matrix, so this is just belt-and-suspenders on the same platform.
47-
# TODO(macos-suite): the first macOS run surfaced ONE environment-specific unit-test
48-
# failure (the install smoke itself is green on macOS; only a temp-path/case-sensitivity
49-
# assumption in a unit test trips on APFS + /tmp→/private/tmp). Pin and fix that test,
50-
# then drop this `if` to get full macOS unit coverage. The install evidence — P2's
51-
# actual deliverable — already runs on macos above.
45+
# Re-run the unit suite here across the matrix. ci.yml already runs it on the Node
46+
# 20/22 Linux matrix; running it on macOS too proves the >=20 behavior on APFS +
47+
# /tmp→/private/tmp, where tmpdir() is a symlink (dryRun now canonicalizes worktree
48+
# paths so per-file attribution string-matches node --test's realpath'd locations).
5249
- name: Test suite
53-
if: matrix.os == 'ubuntu-latest'
5450
run: npm test

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- **`forge imagine --dry-run` per-file attribution on macOS.** `dryRun()` matched node
12+
`--test`'s realpath'd `location:` diagnostics against a raw `mkdtemp` worktree path; on
13+
platforms where `tmpdir()` is itself a symlink (macOS: `/var``/private/var`,
14+
`/tmp``/private/tmp`) the two never matched, so the pass/fail-per-file breakdown was
15+
silently dropped. The worktree root is now canonicalized with `realpathSync` before
16+
comparison (a no-op on Linux). The CI smoke matrix now runs the full unit suite on
17+
`macos-latest`, which surfaced this.
18+
919
## [0.23.0] - 2026-07-19
1020

1121
### Security

src/imagine.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// TAP summary, and always discards the sandbox. Selection stays useful on its own as
99
// "run these, in this order"; dryRun turns the prediction into measured evidence.
1010
import { execFileSync, spawnSync } from "node:child_process";
11-
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
11+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
1212
import { tmpdir } from "node:os";
1313
import { join } from "node:path";
1414
import { build as buildAtlas, impact, load as loadAtlas } from "./atlas.js";
@@ -134,6 +134,12 @@ export function dryRun(root, { tests, timeoutMs = 120000 } = {}) {
134134
const msg = /** @type {{stderr?: Buffer}} */ (e).stderr?.toString().trim() || String(e);
135135
return { ok: false, reason: `git worktree add failed: ${msg}` };
136136
}
137+
// node --test reports each failure's `location:` as a canonical (realpath'd) path.
138+
// On platforms where tmpdir() itself is a symlink (macOS: /var → /private/var), the
139+
// raw `wt` path won't string-match those locations, so per-file attribution below
140+
// must compare against the canonicalized worktree root. On Linux (no symlink) this
141+
// is a no-op.
142+
const wtReal = realpathSync(wt);
137143
// Runner policy: always `node --test <files...>` — a custom package test script
138144
// (jest, vitest, …) is a WHOLE-SUITE command that can't be scoped per-file safely,
139145
// which would defeat minimal selection. We still run node --test and say so, so a
@@ -196,7 +202,7 @@ export function dryRun(root, { tests, timeoutMs = 120000 } = {}) {
196202
let attributable = true;
197203
for (const block of (run.stdout ?? "").split(/^not ok /m).slice(1)) {
198204
const file = block.split("\n").map(locFile).find(Boolean);
199-
const t = file && tests.find((c) => file === join(wt, String(c)));
205+
const t = file && tests.find((c) => file === join(wtReal, String(c)));
200206
if (t) perFile[String(t)] = "fail";
201207
else attributable = false;
202208
}

0 commit comments

Comments
 (0)