enhance(#3618): fold fallow-runner onto the canonical binary resolver (epic #3411 Phase 2) (#3633)

* chore(#3618): fold fallow-runner onto the canonical binary resolver

Epic #3411 Phase 2. src/fallow-runner.cts was the fourth divergent
implementation of Windows binary resolution the epic enumerated —
candidateNames, isExecutableFile, findInPath, findInNodeModules, 40 lines.
All four are deleted; resolveFallowBinary is one seam call.

Two OPT-IN options were added to resolveExecutableBinary to make the fold
behavior-preserving, both defaulting off so Phase 1's callers are byte-identical:

  prependPaths      dirs searched before env.PATH, in order, through the
                    identical per-directory candidate logic. This expresses
                    node_modules/.bin-first precedence without env surgery —
                    the rejected alternative re-introduced the
                    spread-loses-the-proxy hazard the Windows lane caught in
                    Phase 1, at every future call site instead of once.
  requireExecutable POSIX-only accessSync(X_OK); a no-op on win32 where mode
                    bits do not mean execute. Opt-in rather than default
                    because unconditional X_OK breaks #3445's suite, which
                    stages candidates with plain writeFileSync and never sets
                    an exec bit — the repo bans chmod in tests — so every one
                    would resolve to null on POSIX.

Deliberate behavior change on Windows: fallow's prior candidate list ended in a
BARE fallow. The seam never tries a bare name there, so an extensionless file
beside fallow.cmd is no longer resolved. That is the fix, not a regression — the
extensionless file is npm's POSIX sh shim, which CreateProcess cannot run
(#3275). Rows 7 and 8 of the design record it.

Defect found while working, fixed inline: the resolution order was documented
BACKWARDS as PATH-then-.bin in structural-pre-pass.md, docs/INVENTORY.md and
four INVENTORY translations. The code has always been .bin first, and .bin first
is correct — a project-local tool should beat a global one. The archived
changeset is left alone as a historical record.

fallow-runner had no test file at all. tests/fallow-runner.test.cjs is new
(F1-F15) and the seam options are pinned by S1-S12 folded into the existing
dispatch suite. RED proven by execution: with both source files stashed and
build:lib re-run, 7 of 27 probe cases failed.

Refs #3411

* chore(#3618): backfill changeset pr number 3633

* fix(#3618): assert both platform contracts in F4 instead of a POSIX-only premise

Windows CI on #3633 failed F4. The test monkeypatched accessSync to throw and
asserted resolveFallowBinary returned null — but that premise, that the X_OK
check is consulted at all, is POSIX-only by design. requireExecutable is a
deliberate no-op on win32 because Windows mode bits do not mean execute, so the
staged fixture correctly resolved there.

40-design.md's negative-space section already states this carve-out verbatim.
The test contradicted the design it was written from: fixtures were made
platform-adaptive in the previous commit, and this assertion was left
platform-blind.

F4 now asserts BOTH contracts — null on POSIX, resolves on win32 — rather than
skipping either. A t.skip on one lane would have been green and would have left
the win32 carve-out unpinned by fallow's own entry point.

Audited every other row for the same class. F1-F3, F5, F6, F11-F15 hold on both
platforms; F7-F10 and S1-S12 inject platform explicitly and are unaffected. F4
was the only row with a single-platform premise.

The local probe runs on one platform and structurally cannot catch this, which
is why it was green — that limitation is now stated at the top of the probe so a
green probe is not mistaken for platform coverage. The win32 branch was proven
by injecting platform:'win32' with accessSync throwing and asserting it still
resolves.

Refs #3411

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-18 15:32:57 -04:00
committed by GitHub
parent 46f14c621e
commit ac1b6d679f
12 changed files with 614 additions and 59 deletions

View File

@@ -413,6 +413,228 @@ describe('resolveExecutableBinary (#3411)', () => {
});
});
// ─── resolveExecutableBinary seam options (#3618) ───────────────────────────
// prependPaths / requireExecutable — both opt-in, both default off, so
// Phase 1 callers (R1-R25 above, P1-P16 below) stay byte-identical. See
// .gsd/phase/chore-3618-fallow-binary-resolution/50-test-matrix.md (S1-S12).
describe('resolveExecutableBinary seam options (#3618)', () => {
test('S1: prependPaths with binary only in dirA resolves in dirA', () => {
const dirA = createTempDir();
try {
fs.writeFileSync(path.join(dirA, 'foo'), '');
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: '' },
prependPaths: [dirA],
});
assert.equal(resolved, path.join(dirA, 'foo'));
} finally {
cleanup(dirA);
}
});
test('S2: binary in both prependPaths dir and env.PATH — the prependPaths copy wins (precedence)', () => {
const dirA = createTempDir();
const dirB = createTempDir();
try {
fs.writeFileSync(path.join(dirA, 'foo'), '');
fs.writeFileSync(path.join(dirB, 'foo'), '');
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: dirB },
prependPaths: [dirA],
});
assert.equal(resolved, path.join(dirA, 'foo'));
assert.notEqual(resolved, path.join(dirB, 'foo'));
} finally {
cleanup(dirA);
cleanup(dirB);
}
});
test('S3: prependPaths: [] is identical to omitting it', () => {
const dir = createTempDir();
try {
fs.writeFileSync(path.join(dir, 'foo'), '');
const withEmpty = resolveExecutableBinary('foo', { platform: 'linux', env: { PATH: dir }, prependPaths: [] });
const withOmitted = resolveExecutableBinary('foo', { platform: 'linux', env: { PATH: dir } });
assert.equal(withEmpty, path.join(dir, 'foo'));
assert.equal(withEmpty, withOmitted);
} finally {
cleanup(dir);
}
});
test('S4: prependPaths given, binary only on env.PATH — falls through to PATH', () => {
const dirA = createTempDir();
const dirB = createTempDir();
try {
fs.writeFileSync(path.join(dirB, 'foo'), '');
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: dirB },
prependPaths: [dirA],
});
assert.equal(resolved, path.join(dirB, 'foo'));
} finally {
cleanup(dirA);
cleanup(dirB);
}
});
test('S5: prependPaths with two dirs, binary in the second — first-match wins, in array order', () => {
const dir1 = createTempDir();
const dir2 = createTempDir();
try {
fs.writeFileSync(path.join(dir2, 'foo'), '');
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: '' },
prependPaths: [dir1, dir2],
});
assert.equal(resolved, path.join(dir2, 'foo'));
// dir1 is first in the array but has no match, so dir2 wins — confirms
// the search follows array order, not the reverse.
assert.notEqual(resolved, path.join(dir1, 'foo'));
} finally {
cleanup(dir1);
cleanup(dir2);
}
});
test('S6: win32 + prependPaths — PATHEXT applies inside the prepended dir too', () => {
const dir = createTempDir();
try {
fs.writeFileSync(path.join(dir, 'foo.CMD'), '');
const resolved = resolveExecutableBinary('foo', {
platform: 'win32',
env: { PATH: '', PATHEXT: '.EXE;.CMD' },
prependPaths: [dir],
});
assert.equal(resolved, path.join(dir, 'foo.CMD'));
} finally {
cleanup(dir);
}
});
test('S7: requireExecutable true, POSIX, file exists and is executable — resolves', () => {
const dir = createTempDir();
const originalAccessSync = fs.accessSync;
try {
fs.writeFileSync(path.join(dir, 'foo'), '');
// Executability is forced deterministically via accessSync monkeypatch
// rather than chmod — root Docker bypasses mode bits (see file header).
fs.accessSync = () => {};
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: dir },
requireExecutable: true,
});
assert.equal(resolved, path.join(dir, 'foo'));
} finally {
fs.accessSync = originalAccessSync;
cleanup(dir);
}
});
test('S8: requireExecutable true, POSIX, accessSync throws EACCES — null', () => {
const dir = createTempDir();
const originalAccessSync = fs.accessSync;
try {
fs.writeFileSync(path.join(dir, 'foo'), '');
fs.accessSync = () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); };
const resolved = resolveExecutableBinary('foo', {
platform: 'linux',
env: { PATH: dir },
requireExecutable: true,
});
assert.equal(resolved, null);
} finally {
fs.accessSync = originalAccessSync;
cleanup(dir);
}
});
test('S9: requireExecutable omitted — resolves anyway and accessSync is never consulted', () => {
const dir = createTempDir();
const originalAccessSync = fs.accessSync;
let called = false;
try {
fs.writeFileSync(path.join(dir, 'foo'), '');
fs.accessSync = () => {
called = true;
throw Object.assign(new Error('EACCES'), { code: 'EACCES' });
};
const resolved = resolveExecutableBinary('foo', { platform: 'linux', env: { PATH: dir } });
assert.equal(resolved, path.join(dir, 'foo'));
assert.equal(called, false, 'accessSync must not be consulted when requireExecutable is not set');
} finally {
fs.accessSync = originalAccessSync;
cleanup(dir);
}
});
test('S10: requireExecutable true + win32 is a no-op — resolves without consulting accessSync', () => {
const dir = createTempDir();
const originalAccessSync = fs.accessSync;
let called = false;
try {
fs.writeFileSync(path.join(dir, 'foo.CMD'), '');
fs.accessSync = () => {
called = true;
throw Object.assign(new Error('EACCES'), { code: 'EACCES' });
};
const resolved = resolveExecutableBinary('foo', {
platform: 'win32',
env: { PATH: dir, PATHEXT: '.CMD' },
requireExecutable: true,
});
assert.equal(resolved, path.join(dir, 'foo.CMD'));
assert.equal(called, false, 'accessSync must never be consulted on win32');
} finally {
fs.accessSync = originalAccessSync;
cleanup(dir);
}
});
test('S11: requireExecutable true + path-like name — executability still enforced on the direct path', () => {
const dir = createTempDir();
const originalAccessSync = fs.accessSync;
try {
const staged = path.join(dir, 'foo');
fs.writeFileSync(staged, '');
fs.accessSync = () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); };
const resolved = resolveExecutableBinary(staged, {
platform: 'linux',
env: { PATH: '' },
requireExecutable: true,
});
assert.equal(resolved, null);
} finally {
fs.accessSync = originalAccessSync;
cleanup(dir);
}
});
test('S12: neither seam option set behaves exactly as before (win32 and posix)', () => {
const dirWin = createTempDir();
const dirPosix = createTempDir();
try {
fs.writeFileSync(path.join(dirWin, 'foo.CMD'), '');
const win = resolveExecutableBinary('foo', { platform: 'win32', env: { PATH: dirWin, PATHEXT: '.CMD' } });
assert.equal(win, path.join(dirWin, 'foo.CMD'));
fs.writeFileSync(path.join(dirPosix, 'foo'), '');
const posix = resolveExecutableBinary('foo', { platform: 'linux', env: { PATH: dirPosix } });
assert.equal(posix, path.join(dirPosix, 'foo'));
} finally {
cleanup(dirWin);
cleanup(dirPosix);
}
});
});
// ─── projectSpawnInvocation (#3411) ─────────────────────────────────────────
// Mirrors the seam's own `_cmdQuoteToken` (a literal `"` is doubled) so