fix(#4588): observe fork-from-HEAD from prior harness worktrees before degrading (#4868)

* test(#4588): observed fork-from-HEAD must suppress the stale-origin degrade (failing first)

* fix(#4588): observe fork-from-HEAD from prior harness worktrees before degrading

* fix(#4588): a throwing probe git call is an inconclusive observation, not a crash

* test(#4588): name the observed reason in the inconclusive-row assertions

* test(#4588): hermetic state I/O in the inconclusive-row fixtures

* chore(#4588): changeset fragment

* test(#4588): contained cache I/O, emit-payload assertions (review fold-ins)

* fix(#4588): review fold-ins — invalidation fixture, hermetic confirm row, gate comment, cache+scope rows

* chore(#4588): backfill changeset PR number (4868)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-18 23:41:44 -04:00
committed by GitHub
parent 9a41a95212
commit 63edc777e6
3 changed files with 453 additions and 7 deletions

View File

@@ -18,6 +18,8 @@ const fs = require('node:fs');
const path = require('node:path');
const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
const { cleanup } = require('./helpers.cjs');
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const MODULE_PATH = path.join(
__dirname, '..', 'gsd-core', 'bin', 'lib', 'worktree-base-ref.cjs'
@@ -518,8 +520,8 @@ describe('evaluateWorktreeBaseDegrade', () => {
'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null },
}),
});
assert.strictEqual(result.shouldDegrade, true);
assert.strictEqual(result.reason, 'head-diverged-from-fork');
assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
assert.strictEqual(result.headSha, HEAD_SHA);
assert.strictEqual(result.forkRef, 'origin/HEAD');
assert.strictEqual(result.forkSha, FORK_SHA);
@@ -544,8 +546,8 @@ describe('evaluateWorktreeBaseDegrade', () => {
assert.strictEqual(result.forkRef, 'origin/next');
assert.strictEqual(result.forkSha, FORK_SHA);
// HEAD != FORK_SHA in this fixture → degrade
assert.strictEqual(result.shouldDegrade, true);
assert.strictEqual(result.reason, 'head-diverged-from-fork');
assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
assert.ok(result.message !== null);
assert.ok(result.message.includes('origin/next'));
});
@@ -1020,8 +1022,8 @@ describe('evaluateWorktreeBaseDegrade — defensive trim on SHAs (FIX 3)', () =>
'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA + '\r\n', stderr: '', signal: null, error: null },
}),
});
assert.strictEqual(result.shouldDegrade, true);
assert.strictEqual(result.reason, 'head-diverged-from-fork');
assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
// After trimming, headSha and forkSha should be clean
assert.strictEqual(result.headSha, HEAD_SHA);
assert.strictEqual(result.forkSha, FORK_SHA);
@@ -1473,3 +1475,299 @@ describe('#4734: classifyGitHead — single owner of the HEAD-resolution classes
assert.strictEqual(status.headSha, null);
});
});
// ─── #4588 A2: observed fork-from-HEAD confirmation (probe + cache, fail-closed) ──
describe('#4588 A2: a clean prior harness worktree at the orchestrator HEAD confirms fork-from-HEAD', () => {
const HEAD_SHA = 'aabbccdd11223344aabbccdd11223344aabbccdd';
const WT_PATH = '/repo/.claude/worktrees/agent-x';
const ORIGIN_SHA = 'eeee1111223344abeeceeee1111223344abeeceee';
// Worktree-listing stub: one harness worktree at WT_PATH; origin/HEAD DIVERGED
// from HEAD so the pre-#4588 flow would degrade (head-diverged-from-fork).
function makeWorktreeExecGit({ clean = true, wtHead = HEAD_SHA, listTimeout = false } = {}) {
return function stubExecGit(args, _opts) {
const key = args.join(' ');
if (key === 'worktree list --porcelain') {
if (listTimeout) {
const err = new Error('spawnSync git ETIMEDOUT');
err.code = 'ETIMEDOUT';
return { exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: err };
}
return {
exitCode: 0,
stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree ${WT_PATH}\nbranch refs/heads/agent-x\n`,
stderr: '', signal: null, error: null,
};
}
if (key === `-C ${WT_PATH} status --porcelain`) {
return { exitCode: 0, stdout: clean ? '' : ' M tracked.txt', stderr: '', signal: null, error: null };
}
if (key === `-C ${WT_PATH} rev-parse HEAD`) {
return { exitCode: 0, stdout: `${wtHead}\n`, stderr: '', signal: null, error: null };
}
if (key === 'rev-parse HEAD') {
return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'rev-parse --verify --quiet origin/HEAD') {
return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') {
return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null };
}
throw new Error(`Unexpected execGit call: ${JSON.stringify(args)}`);
};
}
test('a clean prior harness worktree at the orchestrator HEAD confirms fork-from-HEAD (no degrade)', () => {
// DIVERGED origin/HEAD: the #3659 flow degrades here unless the probe
// observes that the harness forks from HEAD. All state I/O is in-memory —
// the rows in this describe must stay hermetic (a default fs cache under
// the stub cwd is shared state across tests, and a real fs write at a
// root-level stub path pollutes root CI machines).
let cache = null;
const result = evaluateWorktreeBaseDegrade({
execGit: makeWorktreeExecGit(),
cwd: '/repo',
probeStateRead: () => cache,
probeStateWrite: (_file, content) => { cache = content; },
});
assert.strictEqual(result.shouldDegrade, false, 'a clean worktree at the orchestrator HEAD is positive evidence of fork-from-HEAD');
assert.strictEqual(result.reason, 'fork-from-head-observed');
assert.strictEqual(result.headSha, HEAD_SHA);
});
test('the probe cache serves a matching verdict without re-probing', () => {
const stateRead = (_file) => JSON.stringify({
headSha: HEAD_SHA,
worktreePath: WT_PATH,
worktreeHead: HEAD_SHA,
verdict: 'fork-from-head-confirmed',
probedAt: '2026-09-18T00:00:00.000Z',
});
// The stub answers rev-parse HEAD (classifyGitHead needs it before the
// cache is consulted) and refuses the worktree LIST: if the probe ran,
// this throws and fails.
const execGit = (args) => {
const key = args.join(' ');
if (key === 'rev-parse HEAD') {
return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'worktree list --porcelain') {
throw new Error('probe must not run when the cache matches');
}
throw new Error(`unexpected execGit call: ${key}`);
};
const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo', probeStateRead: stateRead });
assert.strictEqual(result.shouldDegrade, false);
assert.strictEqual(result.reason, 'fork-from-head-observed');
});
test('the cache is invalidated by an orchestrator HEAD move', () => {
const NEW_HEAD = '11223344aabbccdd11223344aabbccdd11223344';
const stateRead = (_file) => JSON.stringify({
headSha: HEAD_SHA,
worktreePath: WT_PATH,
worktreeHead: HEAD_SHA,
verdict: 'fork-from-head-confirmed',
probedAt: '2026-09-18T00:00:00.000Z',
});
// The cached entry is keyed to the OLD orchestrator HEAD while the
// orchestrator HEAD has MOVED (rev-parse HEAD answers NEW_HEAD) and the
// worktree still sits at the old commit — the cache must be ignored, the
// probe re-run, and the flow fall through to the fork comparison.
const execGit = (args) => {
const key = args.join(' ');
if (key === 'rev-parse HEAD') {
return { exitCode: 0, stdout: `${NEW_HEAD}\n`, stderr: '', signal: null, error: null };
}
if (key === 'worktree list --porcelain') {
return {
exitCode: 0,
stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree ${WT_PATH}\nbranch refs/heads/agent-x\n`,
stderr: '', signal: null, error: null,
};
}
if (key === `-C ${WT_PATH} status --porcelain`) {
return { exitCode: 0, stdout: '', stderr: '', signal: null, error: null };
}
if (key === `-C ${WT_PATH} rev-parse HEAD`) {
return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'rev-parse --verify --quiet origin/HEAD') {
return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') {
return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null };
}
throw new Error(`unexpected execGit call: ${key}`);
};
const result = evaluateWorktreeBaseDegrade({
execGit,
cwd: '/repo',
probeStateRead: stateRead,
probeStateWrite: () => {},
});
assert.strictEqual(result.reason, 'head-diverged-from-fork', 'stale cache must not suppress the #3659 comparison');
assert.strictEqual(result.shouldDegrade, true);
});
test('a dirty harness worktree is not evidence', () => {
const result = evaluateWorktreeBaseDegrade({
execGit: makeWorktreeExecGit({ clean: false }),
cwd: '/repo',
probeStateRead: () => null,
probeStateWrite: () => {},
});
assert.strictEqual(result.shouldDegrade, true, `unobserved fork base → the #3659 comparison still governs (reason=${result.reason})`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
});
test('a harness worktree at a different commit is not evidence', () => {
const result = evaluateWorktreeBaseDegrade({
execGit: makeWorktreeExecGit({ wtHead: ORIGIN_SHA }),
cwd: '/repo',
probeStateRead: () => null,
probeStateWrite: () => {},
});
assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
});
test('no harness worktrees changes nothing', () => {
const execGit = makeWorktreeExecGit();
const result = evaluateWorktreeBaseDegrade({
execGit: (args, opts) => {
const key = args.join(' ');
if (key === 'worktree list --porcelain') {
return { exitCode: 0, stdout: 'worktree /repo\nbranch refs/heads/main\n', stderr: '', signal: null, error: null };
}
return execGit(args, opts);
},
cwd: '/repo',
probeStateRead: () => null,
probeStateWrite: () => {},
});
assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`);
assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`);
});
test('a probe timeout fails closed to the existing flow', () => {
const result = evaluateWorktreeBaseDegrade({
execGit: makeWorktreeExecGit({ listTimeout: true }),
cwd: '/repo',
probeStateRead: () => null,
probeStateWrite: () => {},
});
assert.strictEqual(result.shouldDegrade, true);
assert.strictEqual(result.reason, 'head-diverged-from-fork', 'a probe timeout must fall through to the comparison, not skip it');
});
test('untracked-only worktrees count as clean', () => {
const execGit = makeWorktreeExecGit();
const result = evaluateWorktreeBaseDegrade({
execGit: (args, opts) => {
const key = args.join(' ');
if (key === `-C ${WT_PATH} status --porcelain`) {
return { exitCode: 0, stdout: '?? pr-review-notes.md', stderr: '', signal: null, error: null };
}
return execGit(args, opts);
},
cwd: '/repo',
probeStateRead: () => null,
probeStateWrite: () => {},
});
assert.strictEqual(result.shouldDegrade, false, 'untracked review notes do not disqualify the observation');
assert.strictEqual(result.reason, 'fork-from-head-observed');
});
test('orchestrator-worktree mode never probes (the suppress predates the probe)', () => {
const refusing = () => { throw new Error('probe must not run in orchestrator-worktree mode'); };
const result = evaluateWorktreeBaseDegrade({
execGit: refusing,
cwd: '/repo',
effectiveBaseRef: 'head',
isolationMode: 'orchestrator-worktree',
});
assert.strictEqual(result.reason, 'baseref-head');
assert.strictEqual(result.shouldDegrade, false);
});
test('a corrupt cache is re-probed, not trusted', () => {
const stateRead = (_file) => '{ this is not json';
let probed = false;
const execGit = (args) => {
const key = args.join(' ');
if (key === 'worktree list --porcelain') {
probed = true;
return { exitCode: 0, stdout: 'worktree /repo\nbranch refs/heads/main\n', stderr: '', signal: null, error: null };
}
if (key === 'rev-parse HEAD') {
return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'rev-parse --verify --quiet origin/HEAD') {
return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') {
return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null };
}
throw new Error(`unexpected execGit call: ${key}`);
};
const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo', probeStateRead: stateRead });
assert.ok(probed, 'the probe must run past a corrupt cache');
assert.strictEqual(result.reason, 'head-diverged-from-fork', 'corrupt cache → re-probe → the comparison governs');
});
test('worktrees outside the harness directory are not candidates', () => {
const execGit = (args) => {
const key = args.join(' ');
if (key === 'worktree list --porcelain') {
return {
exitCode: 0,
stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree /elsewhere/agent-y\nbranch refs/heads/agent-y\n`,
stderr: '', signal: null, error: null,
};
}
if (key === 'rev-parse HEAD') {
return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'rev-parse --verify --quiet origin/HEAD') {
return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null };
}
if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') {
return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null };
}
throw new Error(`unexpected execGit call: ${key}`);
};
const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo' });
assert.strictEqual(result.reason, 'head-diverged-from-fork', 'a non-harness worktree must not confirm the observation');
});
test('end-to-end: a real repo with a clean harness worktree at HEAD passes the base-check', (t) => {
const { createTempGitProject } = require('./helpers.cjs');
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
const dir = createTempGitProject('gsd-4588-e2e-');
t.after(() => cleanup(dir));
const wtPath = path.join(dir, '.claude', 'worktrees', 'agent-e2e');
fs.mkdirSync(path.dirname(wtPath), { recursive: true });
gitOrThrow(['worktree', 'add', '-b', 'agent-e2e', wtPath, 'HEAD'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
// The command emits its JSON via fs.writeSync(1, …) — capture through the
// approved mock mechanism and assert on the EMITTED payload too (the
// workflow-facing contract), not just the return value.
const emitted = [];
const writeSyncMock = t.mock.method(fs, 'writeSync', (fd, buf, ...rest) => {
void fd; void rest;
emitted.push(typeof buf === 'string' ? buf : Buffer.from(buf).toString('utf8'));
return (typeof buf === 'string' ? buf : Buffer.from(buf)).length;
});
const result = cmdWorktreeBaseCheck(dir, ['--mode', 'harness-worktree']);
assert.ok(writeSyncMock.mock.calls.length > 0, 'the check must emit its JSON payload');
const emittedJson = JSON.parse(emitted.join(''));
assert.strictEqual(emittedJson.reason, 'fork-from-head-observed', 'the emitted workflow payload must carry the observation');
assert.strictEqual(emittedJson.shouldDegrade, false);
assert.strictEqual(result.shouldDegrade, false,
`a clean harness worktree at HEAD must confirm fork-from-HEAD; got reason=${result.reason}`);
assert.strictEqual(result.reason, 'fork-from-head-observed');
});
});