'use strict'; /** * Worktree Base-Ref Module — unit tests * * Seam: gsd-core/bin/lib/worktree-base-ref.cjs * Interface: shortSha, readBaseRefFromSettings, applyWorktreeBaseRef, * resolveEffectiveBaseRef, evaluateWorktreeBaseDegrade * * Issue #683: worktree base-mismatch detection and degradation logic. * All tests use dependency injection (inline stubs) — no real filesystem * or real git is exercised. */ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); 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' ); const { shortSha, readBaseRefFromSettings, applyWorktreeBaseRef, resolveEffectiveBaseRef, evaluateWorktreeBaseDegrade, classifyGitHead, cmdWorktreeBaseCheck, cmdWorktreeSetBaseRef, } = require(MODULE_PATH); // ─── shortSha ──────────────────────────────────────────────────────────────── describe('shortSha', () => { test('returns first 8 chars of a full sha', () => { assert.strictEqual(shortSha('abc123def456789'), 'abc123de'); }); test('returns the string itself when shorter than 8 chars', () => { assert.strictEqual(shortSha('abc12'), 'abc12'); }); test('returns empty string for null', () => { assert.strictEqual(shortSha(null), ''); }); test('returns empty string for empty string', () => { assert.strictEqual(shortSha(''), ''); }); test('returns exactly 8 chars when sha is exactly 8 chars', () => { assert.strictEqual(shortSha('12345678'), '12345678'); }); }); // ─── readBaseRefFromSettings ───────────────────────────────────────────────── describe('readBaseRefFromSettings', () => { test('returns baseRef when present as a string', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: { baseRef: 'head' } }), 'head'); }); test('returns baseRef value "fresh"', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: { baseRef: 'fresh' } }), 'fresh'); }); test('returns null when worktree is missing', () => { assert.strictEqual(readBaseRefFromSettings({}), null); }); test('returns null when settings is null', () => { assert.strictEqual(readBaseRefFromSettings(null), null); }); test('returns null when settings is undefined', () => { assert.strictEqual(readBaseRefFromSettings(undefined), null); }); test('returns null when worktree is not an object (string)', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: 'not-an-object' }), null); }); test('returns null when baseRef is a number (non-string)', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: { baseRef: 42 } }), null); }); test('returns null when baseRef is null', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: { baseRef: null } }), null); }); test('returns null when baseRef is undefined', () => { assert.strictEqual(readBaseRefFromSettings({ worktree: { baseRef: undefined } }), null); }); }); // ─── applyWorktreeBaseRef ───────────────────────────────────────────────────── describe('applyWorktreeBaseRef', () => { test('sets baseRef to "head" when absent, returns changed:true', () => { const settings = {}; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, true); assert.strictEqual(result.skipped, null); assert.strictEqual(result.previous, null); assert.strictEqual(result.settings.worktree.baseRef, 'head'); }); test('sets baseRef to "head" when worktree key is missing entirely', () => { const settings = { other: 'value' }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, true); assert.strictEqual(settings.worktree.baseRef, 'head'); }); test('sets baseRef to "head" when worktree.baseRef is null', () => { const settings = { worktree: { baseRef: null, otherKey: 'keep' } }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, true); assert.strictEqual(settings.worktree.baseRef, 'head'); }); test('sets baseRef to "head" when worktree.baseRef is undefined', () => { const settings = { worktree: { baseRef: undefined } }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, true); assert.strictEqual(settings.worktree.baseRef, 'head'); }); test('preserves other worktree.* keys when setting baseRef', () => { const settings = { worktree: { otherKey: 'preserved', anotherKey: 123 } }; applyWorktreeBaseRef(settings); assert.strictEqual(settings.worktree.otherKey, 'preserved'); assert.strictEqual(settings.worktree.anotherKey, 123); assert.strictEqual(settings.worktree.baseRef, 'head'); }); test('mutates settings in place and returns the same object reference', () => { const settings = {}; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.settings, settings); }); test('returns already-head skip when baseRef is already "head"', () => { const settings = { worktree: { baseRef: 'head' } }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, false); assert.strictEqual(result.skipped, 'already-head'); assert.strictEqual(result.previous, 'head'); assert.strictEqual(settings.worktree.baseRef, 'head'); }); test('returns explicit-other skip when baseRef is "fresh", does NOT overwrite', () => { const settings = { worktree: { baseRef: 'fresh' } }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, false); assert.strictEqual(result.skipped, 'explicit-other'); assert.strictEqual(result.previous, 'fresh'); assert.strictEqual(settings.worktree.baseRef, 'fresh'); }); test('returns explicit-other skip for any other string value', () => { const settings = { worktree: { baseRef: 'some-branch' } }; const result = applyWorktreeBaseRef(settings); assert.strictEqual(result.changed, false); assert.strictEqual(result.skipped, 'explicit-other'); assert.strictEqual(result.previous, 'some-branch'); }); }); // ─── resolveEffectiveBaseRef ────────────────────────────────────────────────── describe('resolveEffectiveBaseRef', () => { // Helper to build a path-keyed readFile stub function makeReadFile(files) { return (p) => (Object.prototype.hasOwnProperty.call(files, p) ? files[p] : null); } test('returns baseRef from settings.local.json when present', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: JSON.stringify({ worktree: { baseRef: 'head' } }), [path.join(claudeDir, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'fresh' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'head'); }); test('falls back to settings.json when settings.local.json has no baseRef', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: JSON.stringify({ other: 'value' }), [path.join(claudeDir, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'fresh' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'fresh'); }); test('returns null when both files are missing', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: () => null }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), null); }); test('returns null when both files exist but have no baseRef', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: JSON.stringify({ other: 'value' }), [path.join(claudeDir, 'settings.json')]: JSON.stringify({ other: 'value2' }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), null); }); test('ignores malformed JSON in settings.local.json and falls back', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: 'not valid json {{{', [path.join(claudeDir, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'head' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'head'); }); test('ignores malformed JSON in settings.json', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: null, [path.join(claudeDir, 'settings.json')]: 'not valid json', }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), null); }); test('settings.local.json null baseRef falls back to settings.json', () => { const claudeDir = '/repo/.claude'; const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: JSON.stringify({ worktree: { baseRef: null } }), [path.join(claudeDir, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'fresh' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'fresh'); }); }); // ─── evaluateWorktreeBaseDegrade ────────────────────────────────────────────── // Stub helper: matches on args.join(' ') and returns canned results. // Module-scoped so the #4734 classifyGitHead describe shares one copy (review finding). function makeExecGit(responses) { return function stubExecGit(args, _opts) { const key = args.join(' '); if (Object.prototype.hasOwnProperty.call(responses, key)) { return responses[key]; } // Default: fail with a helpful error to surface unexpected calls throw new Error(`Unexpected execGit call: ${JSON.stringify(args)}`); }; } describe('evaluateWorktreeBaseDegrade', () => { // #3659 rows share the diverged-HEAD stub shape — one builder keeps the // four fixtures from drifting apart. function makeDivergedExecGit(headSha, forkSha) { const ok = (stdout) => ({ exitCode: 0, stdout, stderr: '', signal: null, error: null }); return makeExecGit({ 'rev-parse HEAD': ok(headSha), 'rev-parse --verify --quiet origin/HEAD': ok(forkSha), }); } test('effectiveBaseRef="head" + orchestrator mode → no degrade, reason baseref-head, execGit never called (#3659)', () => { let called = false; const result = evaluateWorktreeBaseDegrade({ execGit: () => { called = true; return { exitCode: 0, stdout: '', stderr: '', signal: null, error: null }; }, effectiveBaseRef: 'head', isolationMode: 'orchestrator-worktree', }); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'baseref-head'); assert.strictEqual(result.message, null); assert.strictEqual(result.headSha, null); assert.strictEqual(result.forkRef, null); assert.strictEqual(result.forkSha, null); assert.strictEqual(called, false, 'orchestrator mode: GSD controls the fork start-point, head is honored by construction'); }); test('effectiveBaseRef="head" + harness mode (default) + diverged HEAD → degrade, reason baseref-head-ignored-by-harness (#3659)', () => { // #48 verified 5/5 that the harness dispatch path never routes through // project-settings baseRef — with head set on a diverged branch the check // must compare and degrade, not trust the setting. const HEAD_SHA = '11111111223344aa11111111223344aa11111111'; const FORK_SHA = '99999999223344bb99999999223344bb99999999'; const result = evaluateWorktreeBaseDegrade({ execGit: makeDivergedExecGit(HEAD_SHA, FORK_SHA), effectiveBaseRef: 'head', }); assert.strictEqual(result.shouldDegrade, true, 'head must not suppress the comparison in harness mode (#3659)'); assert.strictEqual(result.reason, 'baseref-head-ignored-by-harness'); assert.strictEqual(result.headSha, HEAD_SHA); assert.strictEqual(result.forkRef, 'origin/HEAD'); assert.strictEqual(result.forkSha, FORK_SHA); }); test('effectiveBaseRef="head" + explicit harness-worktree mode + diverged → degrade (#3659)', () => { const HEAD_SHA = 'aaaa1111223344ccaaaa1111223344ccaaaa1111'; const FORK_SHA = 'bbbb1111223344ddbbbb1111223344ddbbbb1111'; const result = evaluateWorktreeBaseDegrade({ execGit: makeDivergedExecGit(HEAD_SHA, FORK_SHA), effectiveBaseRef: 'head', isolationMode: 'harness-worktree', }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'baseref-head-ignored-by-harness'); }); test('effectiveBaseRef="head" + harness + HEAD == origin/HEAD → no degrade, reason head-matches-fork (#3659)', () => { const SAME_SHA = 'cccc1111223344eecccc1111223344eecccc1111'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: SAME_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: SAME_SHA, stderr: '', signal: null, error: null }, }), effectiveBaseRef: 'head', isolationMode: 'harness-worktree', }); assert.strictEqual(result.shouldDegrade, false, 'when the harness fork base happens to equal HEAD there is no mismatch to degrade for'); assert.strictEqual(result.reason, 'head-matches-fork'); }); test('harness head-diverge message cites the verified harness limitation (#3659)', () => { const HEAD_SHA = 'dddd1111223344ffdddd1111223344ffdddd1111'; const FORK_SHA = 'eeee1111223344abeeceeee1111223344abeeceee'; const result = evaluateWorktreeBaseDegrade({ execGit: makeDivergedExecGit(HEAD_SHA, FORK_SHA), effectiveBaseRef: 'head', }); assert.ok(result.message !== null, 'divergence under head must carry the explanatory message'); assert.ok(result.message.includes('#48'), 'message must cite the verified harness limitation'); assert.ok(result.message.includes('sequentially'), 'message must state the sequential fallback'); }); test('git rev-parse HEAD exits 128 (definitive no-repository) → degrades, reason no-head (#4734)', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 128, stdout: '', stderr: 'fatal: not a git repo', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'no-head'); assert.strictEqual(result.headSha, null); }); test('git rev-parse HEAD returns empty stdout → no degrade, reason no-head', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: '', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'no-head'); }); // ─── #3050: fail-closed matrix for git rev-parse HEAD outcomes ───────────── // DECIDED RULE: degrade UNLESS git completed and gave a definitive answer. // - timeout → degrade, reason 'head-unresolvable' // (#4734 revised the exit-128 row: git's definitive no-repository answer // now DEGRADES — a worktree can never be created there. The ambiguous // exit-0-empty row is unchanged, still deliberately non-degrading.) // - exitCode === 128 → degrade, reason 'no-head' (#4734) // - exit 0 with non-empty sha → proceed (unchanged) // - anything else (127, other → degrade, reason 'head-unresolvable' // non-zero, exit 0 empty stdout // is pinned separately above) test('git rev-parse HEAD TIMES OUT → shouldDegrade:true, reason "head-unresolvable" (#3050)', () => { const timedOutErr = new Error('spawnSync git ETIMEDOUT'); timedOutErr.code = 'ETIMEDOUT'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: timedOutErr }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'head-unresolvable'); assert.ok(result.message, 'a fail-closed degrade must carry a non-null explanatory message'); assert.strictEqual(result.headSha, null); }); test('cross-platform: timeout WITHOUT signal set (Windows shape) still degrades (#3050)', () => { // Node.js guarantees error.code === 'ETIMEDOUT' cross-platform when the // spawnSync `timeout` option fires; `signal` reporting is the // platform-fragile half and must not be required to detect a timeout. const timedOutErr = new Error('spawnSync git ETIMEDOUT'); timedOutErr.code = 'ETIMEDOUT'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: null, stdout: '', stderr: '', signal: null, error: timedOutErr }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'head-unresolvable'); }); test('git missing (exitCode 127) → degrade, reason "head-unresolvable" (#3050)', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 127, stdout: '', stderr: 'git: not found', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'head-unresolvable'); }); // Boundary coverage: 128 is the ONLY benign non-zero exit (definitive "not a // git repository"). 129 (limit+1) must NOT be swept into that carve-out. test('exitCode 129 (limit+1 boundary, just past the 128 carve-out) → degrade, reason "head-unresolvable" (#3050)', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 129, stdout: '', stderr: 'fatal: something else', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'head-unresolvable'); }); test('other non-zero, non-128 exit → degrade, reason "head-unresolvable" (#3050)', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 1, stdout: '', stderr: 'fatal: something else', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'head-unresolvable'); }); test('exitCode 128 ("not a git repository") degrades with a user-visible message (#4734; was a #3050 non-degrade pin)', () => { const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'no-head'); assert.strictEqual(result.headAbsenceVerified, true); // Message presence is a typed fact (non-null, non-empty) — its prose is // operator-facing text, not a test oracle (no raw-text matching). assert.ok(typeof result.message === 'string' && result.message.length > 0, 'the degrade carries the message the workflow prints'); }); // ─── #3057 B8: headAbsenceVerified distinguishes the two "no-head" causes ── // // #4734 revised the exit-128 outcome (degrade, with headAbsenceVerified:true // preserved) and left the exit-0-empty outcome deliberately unchanged — the // paired tests below prove both, and that a caller can still tell the two // 'no-head' causes apart. test('exit 128 — git\'s definitive "not a git repository" answer → degrades, headAbsenceVerified:true (#4734)', () => { const faultyGit = makeFaultyGit({ faults: [{ kind: 'exit', exitCode: 128, stderr: 'fatal: not a git repository' }], }); const result = evaluateWorktreeBaseDegrade({ execGit: faultyGit }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'no-head'); assert.strictEqual(result.headAbsenceVerified, true); }); test('exit 0 with empty stdout — git completed but gave no useful answer → headAbsenceVerified:false', () => { // makeFaultyGit()'s default passthrough IS exit 0 / empty stdout / no // error / not timed out — a real, completed, but non-substantive answer. const faultyGit = makeFaultyGit(); const result = evaluateWorktreeBaseDegrade({ execGit: faultyGit }); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'no-head'); assert.strictEqual(result.headAbsenceVerified, false); }); test('headAbsenceVerified is null (not applicable) for a reason other than no-head', () => { const result = evaluateWorktreeBaseDegrade({ effectiveBaseRef: 'head', isolationMode: 'orchestrator-worktree' }); assert.strictEqual(result.reason, 'baseref-head'); assert.strictEqual(result.headAbsenceVerified, null); }); test('HEAD == origin/HEAD → no degrade, reason head-matches-fork', () => { const HEAD_SHA = 'aabbccdd11223344aabbccdd11223344aabbccdd'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'head-matches-fork'); assert.strictEqual(result.headSha, HEAD_SHA); assert.strictEqual(result.forkRef, 'origin/HEAD'); assert.strictEqual(result.forkSha, HEAD_SHA); assert.strictEqual(result.message, null); }); test('HEAD != origin/HEAD → degrade, reason head-diverged-from-fork, MSG_DIVERGED', () => { const HEAD_SHA = 'deadbeef11223344deadbeef11223344deadbeef'; const FORK_SHA = 'cafebabe11223344cafebabe11223344cafebabe'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null }, }), }); 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); // Verify message contains the short SHAs and the corrected remediation // (#3659: the old text advised setting baseRef:"head", which the harness // does not read). const expectedMsg = `⚠ Worktree base mismatch: HEAD (${HEAD_SHA.slice(0, 8)}) differs from origin/HEAD (${FORK_SHA.slice(0, 8)}). Running this phase sequentially on the main working tree. Parallel worktrees return once HEAD is merged/pushed so origin/HEAD matches it. (worktree.baseRef:"head" applies only where GSD itself creates the worktree — the runtime harness does not read it; #48, #3659.)`; assert.strictEqual(result.message, expectedMsg); }); test('origin/HEAD fails but symbolic-ref resolves to refs/remotes/origin/next', () => { const HEAD_SHA = 'aaaa1111bbbb2222aaaa1111bbbb2222aaaa1111'; const FORK_SHA = 'cccc3333dddd4444cccc3333dddd4444cccc3333'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, 'symbolic-ref --quiet refs/remotes/origin/HEAD': { exitCode: 0, stdout: 'refs/remotes/origin/next', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet refs/remotes/origin/next': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.forkRef, 'origin/next'); assert.strictEqual(result.forkSha, FORK_SHA); // HEAD != FORK_SHA in this fixture → degrade 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')); }); test('origin/HEAD fails AND symbolic-ref fails → degrade, reason fork-ref-unknown, MSG_UNKNOWN', () => { const HEAD_SHA = 'eeee5555ffff6666eeee5555ffff6666eeee5555'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, 'symbolic-ref --quiet refs/remotes/origin/HEAD': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'fork-ref-unknown'); assert.strictEqual(result.forkRef, null); assert.strictEqual(result.forkSha, null); const expectedMsg = `⚠ Cannot determine the worktree fork base (origin/HEAD unresolved). Running this phase sequentially on the main working tree to avoid a base mismatch. Parallel worktrees return once origin/HEAD resolves and matches HEAD. See #683, #3659.`; assert.strictEqual(result.message, expectedMsg); }); test('cwd is passed through to execGit calls', () => { const HEAD_SHA = '1234567890abcdef1234567890abcdef12345678'; const capturedOpts = []; const result = evaluateWorktreeBaseDegrade({ cwd: '/some/worktree', execGit: (args, opts) => { capturedOpts.push(opts); const key = args.join(' '); if (key === 'rev-parse HEAD') return { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }; if (key === 'rev-parse --verify --quiet origin/HEAD') return { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }; throw new Error(`Unexpected: ${key}`); }, }); assert.strictEqual(result.shouldDegrade, false); assert.ok(capturedOpts.length > 0); for (const opts of capturedOpts) { assert.strictEqual(opts && opts.cwd, '/some/worktree'); } }); test('symbolic-ref resolves but subsequent rev-parse fails → falls through to fork-ref-unknown', () => { const HEAD_SHA = 'abcd1234abcd1234abcd1234abcd1234abcd1234'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, 'symbolic-ref --quiet refs/remotes/origin/HEAD': { exitCode: 0, stdout: 'refs/remotes/origin/main', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet refs/remotes/origin/main': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'fork-ref-unknown'); assert.strictEqual(result.forkRef, null); assert.strictEqual(result.forkSha, null); }); }); // ─── cmdWorktreeBaseCheck ───────────────────────────────────────────────────── describe('cmdWorktreeBaseCheck', () => { function makeExecGitCheck(responses) { return function stubExecGit(args, _opts) { const key = args.join(' '); if (Object.prototype.hasOwnProperty.call(responses, key)) { return responses[key]; } throw new Error(`Unexpected execGit call: ${JSON.stringify(args)}`); }; } test('baseRef=head in settings + --mode orchestrator-worktree → shouldDegrade false, reason baseref-head; write emits valid JSON (#3659)', () => { const cwd = '/repo'; const claudeDir = '/repo/.claude'; let written = ''; const deps = { readFile: (p) => { if (p === path.join(claudeDir, 'settings.local.json')) return JSON.stringify({ worktree: { baseRef: 'head' } }); return null; }, execGit: makeExecGitCheck({}), write: (s) => { written += s; }, // Hermetic: point userClaudeDir at a non-existent path so real ~/.claude is never read userClaudeDir: '/nonexistent-hermetic-user-dir', }; const result = cmdWorktreeBaseCheck(cwd, ['--mode', 'orchestrator-worktree'], deps); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'baseref-head'); const parsed = JSON.parse(written); assert.deepStrictEqual(parsed, result); }); test('baseRef=head in settings + default (harness) mode + diverged HEAD → shouldDegrade true (#3659)', () => { const cwd = '/repo'; const claudeDir = '/repo/.claude'; const HEAD_SHA = 'fade1111223344cafade1111223344cafade1111'; const FORK_SHA = 'bead1111223344dbbead1111223344dbbead1111'; const deps = { readFile: (p) => { if (p === path.join(claudeDir, 'settings.local.json')) return JSON.stringify({ worktree: { baseRef: 'head' } }); return null; }, execGit: makeExecGitCheck({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null }, }), write: () => {}, userClaudeDir: '/nonexistent-hermetic-user-dir', }; const result = cmdWorktreeBaseCheck(cwd, [], deps); assert.strictEqual(result.shouldDegrade, true, 'settings head must not suppress the harness-mode comparison (#3659)'); assert.strictEqual(result.reason, 'baseref-head-ignored-by-harness'); }); test('--mode rejects invalid values — no silent default that would re-open the #3659 hole', () => { const cwd = '/repo'; const deps = { readFile: () => null, execGit: makeExecGitCheck({}), write: () => {}, userClaudeDir: '/nonexistent-hermetic-user-dir', }; assert.throws( () => cmdWorktreeBaseCheck(cwd, ['--mode', 'bogus-mode'], deps), /--mode must be harness-worktree or orchestrator-worktree/ ); assert.throws( () => cmdWorktreeBaseCheck(cwd, ['--mode'], deps), /--mode must be harness-worktree or orchestrator-worktree/ ); }); test('default emit goes through fs.writeSync(1, …) — the seam --pick intercepts (#3659)', (t) => { // The CLI's --pick capture patches fs.writeSync, not process.stdout.write; // under $(…) command substitution the stdout.write default made --pick // emit the full JSON, so the workflow auto-degrade guards never matched. const fds = []; const chunks = []; const original = fs.writeSync; fs.writeSync = (fd, buf, offset, length) => { const n = original.call(fs, fd, buf, offset, length); fds.push(fd); // Chunk is derived from the REAL return count `n`, not the requested // extent — a genuine short write must be reflected accurately (#4306). const start = offset ?? 0; chunks.push(typeof buf === 'string' ? buf.slice(start, start + n) : buf.toString('utf8', start, start + n)); return n; }; t.after(() => { fs.writeSync = original; }); const result = cmdWorktreeBaseCheck('/repo', [], { readFile: () => null, execGit: makeExecGitCheck({ 'rev-parse HEAD': { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository', signal: null, error: null }, }), userClaudeDir: '/nonexistent-hermetic-user-dir', // no deps.write — the default seam under test }); assert.ok(fds.length > 0, 'default emit must call fs.writeSync'); assert.ok(fds.every((fd) => fd === 1), 'every write must target fd 1 (stdout)'); const emitted = chunks.join(''); assert.ok(emitted.includes('"reason"'), 'emitted payload is the JSON result'); assert.strictEqual(result.reason, 'no-head'); }); test('diverged shas → shouldDegrade true; captured JSON parses correctly', () => { const cwd = '/repo'; const HEAD_SHA = 'deadbeef11223344deadbeef11223344deadbeef'; const FORK_SHA = 'cafebabe11223344cafebabe11223344cafebabe'; let written = ''; const deps = { readFile: () => null, execGit: makeExecGitCheck({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null }, }), write: (s) => { written += s; }, // Hermetic: point userClaudeDir at a non-existent path so real ~/.claude is never read userClaudeDir: '/nonexistent-hermetic-user-dir', }; const result = cmdWorktreeBaseCheck(cwd, [], deps); assert.strictEqual(result.shouldDegrade, true); const parsed = JSON.parse(written); assert.strictEqual(parsed.shouldDegrade, true); assert.strictEqual(parsed.reason, 'head-diverged-from-fork'); }); }); // ─── cmdWorktreeSetBaseRef ──────────────────────────────────────────────────── describe('cmdWorktreeSetBaseRef', () => { test('readFile returns {} → changed true, writeFile called with worktree.baseRef "head"', () => { const cwd = '/repo'; const file = path.join(cwd, '.claude', 'settings.local.json'); let writtenPath = null; let writtenContent = null; let written = ''; const deps = { readFile: () => '{}', existsSync: () => true, mkdir: () => {}, writeFile: (p, content) => { writtenPath = p; writtenContent = content; }, write: (s) => { written += s; }, }; const result = cmdWorktreeSetBaseRef(cwd, [], deps); assert.strictEqual(result.changed, true); assert.strictEqual(result.file, file); assert.strictEqual(result.baseRef, 'head'); assert.strictEqual(writtenPath, file); const parsedWritten = JSON.parse(writtenContent); assert.strictEqual(parsedWritten.worktree.baseRef, 'head'); const parsedOutput = JSON.parse(written); assert.strictEqual(parsedOutput.changed, true); }); test('readFile returns explicit-other → changed false, skipped explicit-other, writeFile NOT called', () => { const cwd = '/repo'; let writeFileCalled = false; let written = ''; const deps = { readFile: () => JSON.stringify({ worktree: { baseRef: 'fresh' } }), existsSync: () => true, mkdir: () => {}, writeFile: () => { writeFileCalled = true; }, write: (s) => { written += s; }, }; const result = cmdWorktreeSetBaseRef(cwd, [], deps); assert.strictEqual(result.changed, false); assert.strictEqual(result.skipped, 'explicit-other'); assert.strictEqual(result.previous, 'fresh'); assert.strictEqual(writeFileCalled, false, 'writeFile must NOT be called for explicit-other'); const parsedOutput = JSON.parse(written); assert.strictEqual(parsedOutput.changed, false); assert.strictEqual(parsedOutput.skipped, 'explicit-other'); }); test('readFile returns malformed JSON → throws refusing-to-modify error', () => { const cwd = '/repo'; const file = path.join(cwd, '.claude', 'settings.local.json'); const deps = { readFile: () => '{', existsSync: () => true, mkdir: () => {}, writeFile: () => {}, write: () => {}, }; assert.throws( () => cmdWorktreeSetBaseRef(cwd, [], deps), (err) => { assert.ok(err instanceof Error, 'must throw an Error'); assert.ok(err.message.includes('Refusing to modify'), `message should contain "Refusing to modify", got: ${err.message}`); assert.ok(err.message.includes(file), `message should contain file path, got: ${err.message}`); return true; } ); }); test('readFile returns null (missing file) → treated as {} → changed true', () => { const cwd = '/repo'; let writeFileCalled = false; const deps = { readFile: () => null, existsSync: () => false, mkdir: () => {}, writeFile: () => { writeFileCalled = true; }, write: () => {}, }; const result = cmdWorktreeSetBaseRef(cwd, [], deps); assert.strictEqual(result.changed, true); assert.strictEqual(writeFileCalled, true); }); // FIX 2: non-object top-level JSON must be rejected with a clear error test('readFile returns "[]" (array) → throws /expected a JSON object/', () => { const cwd = '/repo'; const deps = { readFile: () => '[]', existsSync: () => true, mkdir: () => {}, writeFile: () => {}, write: () => {}, }; assert.throws( () => cmdWorktreeSetBaseRef(cwd, [], deps), /expected a JSON object/ ); }); test('readFile returns "42" (primitive) → throws /expected a JSON object/', () => { const cwd = '/repo'; const deps = { readFile: () => '42', existsSync: () => true, mkdir: () => {}, writeFile: () => {}, write: () => {}, }; assert.throws( () => cmdWorktreeSetBaseRef(cwd, [], deps), /expected a JSON object/ ); }); }); // FIX 2: applyWorktreeBaseRef must reject non-object/array/null inputs describe('applyWorktreeBaseRef — non-object inputs (FIX 2)', () => { test('applyWorktreeBaseRef(null) → throws TypeError', () => { assert.throws( () => applyWorktreeBaseRef(null), TypeError ); }); test('applyWorktreeBaseRef([]) → throws TypeError', () => { assert.throws( () => applyWorktreeBaseRef([]), TypeError ); }); }); // ─── FIX 2: JSONC support ───────────────────────────────────────────────────── describe('resolveEffectiveBaseRef — JSONC (FIX 2)', () => { function makeReadFile(files) { return (p) => (Object.prototype.hasOwnProperty.call(files, p) ? files[p] : null); } test('returns baseRef from settings.local.json with // line comments', () => { const claudeDir = '/repo/.claude'; const jsonc = [ '// this is a comment', '{', ' // another comment', ' "worktree": {', ' "baseRef": "head" // inline comment', ' }', '}', ].join('\n'); const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: jsonc, }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'head'); }); test('returns baseRef from settings.local.json with /* */ block comments', () => { const claudeDir = '/repo/.claude'; const jsonc = [ '/* block comment */', '{', ' "worktree": { /* inline block */ "baseRef": "fresh" }', '}', '/* trailing block */', ].join('\n'); const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: jsonc, }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps), 'fresh'); }); }); describe('cmdWorktreeSetBaseRef — JSONC (FIX 2)', () => { test('commented-but-valid settings.local.json → updates it (changed true) rather than throwing', () => { const cwd = '/repo'; const jsonc = [ '// user comment', '{', ' // another comment', ' "other": "value"', '}', ].join('\n'); let writtenContent = null; const deps = { readFile: () => jsonc, existsSync: () => true, mkdir: () => {}, writeFile: (_p, content) => { writtenContent = content; }, write: () => {}, }; const result = cmdWorktreeSetBaseRef(cwd, [], deps); assert.strictEqual(result.changed, true, 'must set baseRef when absent (even in JSONC file)'); assert.ok(writtenContent !== null, 'must write the updated file'); const parsed = JSON.parse(writtenContent); assert.strictEqual(parsed.worktree.baseRef, 'head'); }); test('JSONC with explicit baseRef="fresh" → skipped explicit-other, does not throw', () => { const cwd = '/repo'; const jsonc = [ '// user comment', '{', ' "worktree": {', ' // keeps the fork base fixed', ' "baseRef": "fresh"', ' }', '}', ].join('\n'); let writeFileCalled = false; const deps = { readFile: () => jsonc, existsSync: () => true, mkdir: () => {}, writeFile: () => { writeFileCalled = true; }, write: () => {}, }; const result = cmdWorktreeSetBaseRef(cwd, [], deps); assert.strictEqual(result.changed, false); assert.strictEqual(result.skipped, 'explicit-other'); assert.strictEqual(writeFileCalled, false); }); test('genuinely malformed JSON (after stripping comments) still throws refusing-to-modify', () => { const cwd = '/repo'; const file = path.join(cwd, '.claude', 'settings.local.json'); // This is malformed even after comment stripping const malformed = '// comment\n{ "key": }'; const deps = { readFile: () => malformed, existsSync: () => true, mkdir: () => {}, writeFile: () => {}, write: () => {}, }; assert.throws( () => cmdWorktreeSetBaseRef(cwd, [], deps), (err) => { assert.ok(err instanceof Error); assert.ok(err.message.includes('Refusing to modify'), `got: ${err.message}`); assert.ok(err.message.includes(file), `got: ${err.message}`); return true; } ); }); }); // ─── FIX 3: defensive trim on git SHAs ──────────────────────────────────────── describe('evaluateWorktreeBaseDegrade — defensive trim on SHAs (FIX 3)', () => { function makeExecGit(responses) { return function stubExecGit(args, _opts) { const key = args.join(' '); if (Object.prototype.hasOwnProperty.call(responses, key)) { return responses[key]; } throw new Error(`Unexpected execGit call: ${JSON.stringify(args)}`); }; } test('HEAD with trailing newline still matches origin/HEAD — no degrade', () => { const SHA = 'aabbccdd11223344aabbccdd11223344aabbccdd'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: SHA + '\n', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: SHA + '\n', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.shouldDegrade, false); assert.strictEqual(result.reason, 'head-matches-fork'); }); test('HEAD with trailing whitespace still diverges correctly from different origin/HEAD', () => { const HEAD_SHA = 'deadbeef11223344deadbeef11223344deadbeef'; const FORK_SHA = 'cafebabe11223344cafebabe11223344cafebabe'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA + '\n', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA + '\r\n', stderr: '', signal: null, error: null }, }), }); 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); }); test('symbolic-ref stdout with trailing newline resolves correctly', () => { const HEAD_SHA = 'aaaa1111bbbb2222aaaa1111bbbb2222aaaa1111'; const FORK_SHA = 'cccc3333dddd4444cccc3333dddd4444cccc3333'; const result = evaluateWorktreeBaseDegrade({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: HEAD_SHA + '\n', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet origin/HEAD': { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }, 'symbolic-ref --quiet refs/remotes/origin/HEAD': { exitCode: 0, stdout: 'refs/remotes/origin/next\n', stderr: '', signal: null, error: null }, 'rev-parse --verify --quiet refs/remotes/origin/next': { exitCode: 0, stdout: FORK_SHA + '\n', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(result.forkRef, 'origin/next'); assert.strictEqual(result.forkSha, FORK_SHA); assert.strictEqual(result.shouldDegrade, true); }); }); // ─── resolveEffectiveBaseRef — user/global layer (#1013) ───────────────────── describe('resolveEffectiveBaseRef — user/global layer (#1013)', () => { function makeReadFile(files) { return (p) => (Object.prototype.hasOwnProperty.call(files, p) ? files[p] : null); } const USER_CLAUDE_DIR = '/home/user/.claude'; const claudeDir = '/repo/.claude'; test('(a) user/global settings.json provides baseRef:"head" when both project files absent', () => { const deps = { readFile: makeReadFile({ [path.join(USER_CLAUDE_DIR, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'head' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, USER_CLAUDE_DIR), 'head'); }); test('(b) project local "fresh" OVERRIDES user/global "head" → returns "fresh"', () => { const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.local.json')]: JSON.stringify({ worktree: { baseRef: 'fresh' } }), [path.join(USER_CLAUDE_DIR, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'head' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, USER_CLAUDE_DIR), 'fresh'); }); test('(c) project shared "fresh" (no local) OVERRIDES user/global "head" → returns "fresh"', () => { const deps = { readFile: makeReadFile({ [path.join(claudeDir, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'fresh' } }), [path.join(USER_CLAUDE_DIR, 'settings.json')]: JSON.stringify({ worktree: { baseRef: 'head' } }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, USER_CLAUDE_DIR), 'fresh'); }); test('(d) userClaudeDir undefined → behaves as before, returns null when both project files absent', () => { const deps = { readFile: () => null }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, undefined), null); }); test('(d) userClaudeDir null → behaves as before, returns null when both project files absent', () => { const deps = { readFile: () => null }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, null), null); }); test('user/global settings.json absent → returns null (no fallback beyond user layer)', () => { const deps = { readFile: makeReadFile({ // user settings.json present but has no baseRef [path.join(USER_CLAUDE_DIR, 'settings.json')]: JSON.stringify({ other: 'value' }), }), }; assert.strictEqual(resolveEffectiveBaseRef(claudeDir, deps, USER_CLAUDE_DIR), null); }); test('userClaudeDir === claudeDir → does not double-read (avoids re-reading shared settings.json)', () => { // When project dir IS the user dir (cwd is home), the user layer should be skipped // to avoid reading settings.json twice. This is enforced by the path.resolve comparison. const sameDir = '/home/.claude'; let readCount = 0; const deps = { readFile: (p) => { readCount++; if (p === path.join(sameDir, 'settings.local.json')) return null; if (p === path.join(sameDir, 'settings.json')) return JSON.stringify({ worktree: { baseRef: 'head' } }); return null; }, }; // resolveEffectiveBaseRef(sameDir, deps, sameDir) — userClaudeDir === claudeDir const result = resolveEffectiveBaseRef(sameDir, deps, sameDir); assert.strictEqual(result, 'head'); // still reads shared settings.json (the project layer) // The shared settings.json should have been read exactly once (project layer), not twice assert.strictEqual(readCount, 2, 'only local + shared should be read; user layer skipped when same dir'); }); }); // ─── cmdWorktreeBaseCheck — user/global cascade (#1013 KEY REGRESSION) ─────── describe('cmdWorktreeBaseCheck — user/global cascade (#1013)', () => { // Phase-lane execGit: origin/HEAD probe fails (no symref either) → fork-ref-unknown → degrade function makePhaseLaneExecGit(HEAD_SHA) { return function stubExecGit(args, _opts) { const key = args.join(' '); if (key === 'rev-parse HEAD') { return { exitCode: 0, stdout: HEAD_SHA, stderr: '', signal: null, error: null }; } if (key === 'rev-parse --verify --quiet origin/HEAD') { return { exitCode: 1, stdout: '', 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)}`); }; } const HEAD_SHA = 'phase1lane11223344phase1lane11223344phase'; const USER_CLAUDE_DIR = '/home/user/.claude'; const cwd = '/repo'; const claudeDir = '/repo/.claude'; test('(e positive) user/global head + phase lane + orchestrator mode → shouldDegrade:false (KEY REGRESSION #1013)', () => { // This is the exact bug #1013 fixed: user set worktree.baseRef:"head" in their global // settings, but the setting was invisible and the phase lane triggered degrade. The // suppress now applies only where GSD manages the fork (--mode orchestrator-worktree), // which is also what keeps this cascade proof meaningful post-#3659. const deps = { execGit: makePhaseLaneExecGit(HEAD_SHA), readFile: (p) => { // Project files: no baseRef if (p === path.join(claudeDir, 'settings.local.json')) return null; if (p === path.join(claudeDir, 'settings.json')) return null; // User/global file: baseRef = "head" if (p === path.join(USER_CLAUDE_DIR, 'settings.json')) { return JSON.stringify({ worktree: { baseRef: 'head' } }); } return null; }, write: () => {}, userClaudeDir: USER_CLAUDE_DIR, }; const result = cmdWorktreeBaseCheck(cwd, ['--mode', 'orchestrator-worktree'], deps); assert.strictEqual(result.shouldDegrade, false, 'user/global worktree.baseRef:"head" must suppress degrade on a phase lane where GSD manages the fork'); assert.strictEqual(result.reason, 'baseref-head'); }); test('user/global head + phase lane + default (harness) mode → shouldDegrade:true (#3659)', () => { // The mirror of the KEY REGRESSION row: in harness mode the setting cannot // suppress anything (#48), so the same lane degrades. const deps = { execGit: makePhaseLaneExecGit(HEAD_SHA), readFile: (p) => { if (p === path.join(claudeDir, 'settings.local.json')) return null; if (p === path.join(claudeDir, 'settings.json')) return null; if (p === path.join(USER_CLAUDE_DIR, 'settings.json')) { return JSON.stringify({ worktree: { baseRef: 'head' } }); } return null; }, write: () => {}, userClaudeDir: USER_CLAUDE_DIR, }; const result = cmdWorktreeBaseCheck(cwd, [], deps); assert.strictEqual(result.shouldDegrade, true, 'harness mode: head must not suppress the lane degrade (#3659)'); assert.strictEqual(result.reason, 'fork-ref-unknown'); }); test('(e negative) NO user/global head + same phase lane → shouldDegrade:true (proves lane degrades)', () => { // Without a user/global head, the phase lane must still degrade (proves the positive test is real) const deps = { execGit: makePhaseLaneExecGit(HEAD_SHA), readFile: () => null, // no project or user settings write: () => {}, userClaudeDir: '/nonexistent-hermetic-dir-no-global', }; const result = cmdWorktreeBaseCheck(cwd, [], deps); assert.strictEqual(result.shouldDegrade, true, 'without user/global head, a phase lane must degrade'); assert.strictEqual(result.reason, 'fork-ref-unknown'); }); }); // ─── workflow dispatch-site coverage: worktree.base-check gates (folded from // fix-1941-quick-worktree-stale-base.test.cjs and // fix-2649-diagnose-issues-worktree-stale-base.test.cjs, #3335) ────────────── // // allow-test-rule: source-text-is-the-product #1941 #2649 // Workflow .md files are the installed AI instructions — their text IS what the // runtime loads. Testing text content tests the deployed contract. Per // CONTRIBUTING.md exception matrix. // // Root cause shared by #1941 and #2649: Claude Code's isolation="worktree" // forks new worktrees from origin/HEAD, not the live local HEAD. When prior // local commits advance local HEAD without an intervening `git push`, // origin/HEAD stays pinned to a stale ancestor and the executor's // worktree_branch_check guard halts with a base-mismatch fatal. The fix ports // the worktree.base-check auto-degrade pattern (originally execute-phase // #683/#1369) into each not-yet-covered dispatch site: quick.md's // single-dispatch path (#1941), and diagnose-issues.md's spawn_agents step // plus execute-plan.md's Pattern A single-plan dispatch (#2649, fixed // together per that bug's acceptance criterion 5 — same bug class, same // one-line gate). { const { describe: __foldDescribe } = require('node:test'); __foldDescribe('folded:fix-1941-quick-worktree-stale-base', () => { const QUICK_WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'quick.md'); describe('quick: pre-dispatch worktree base re-check (#1941)', () => { test('workflow file exists', () => { assert.ok(fs.existsSync(QUICK_WORKFLOW_PATH), 'workflows/quick.md should exist'); }); test('Step 6 runs worktree.base-check before capturing EXPECTED_BASE', () => { const content = fs.readFileSync(QUICK_WORKFLOW_PATH, 'utf-8'); const step6Idx = content.indexOf('**Step 6: Spawn executor**'); const baseCheckIdx = content.indexOf('worktree.base-check', step6Idx); const expectedBaseIdx = content.indexOf('EXPECTED_BASE=$(git rev-parse HEAD)', step6Idx); assert.ok(step6Idx !== -1, '"Step 6: Spawn executor" must exist in quick.md'); assert.ok(baseCheckIdx !== -1, 'worktree.base-check must be invoked within Step 6'); assert.ok(expectedBaseIdx !== -1, 'EXPECTED_BASE capture must exist within Step 6'); assert.ok( baseCheckIdx < expectedBaseIdx, 'worktree.base-check must run BEFORE EXPECTED_BASE is captured so the degrade decision reflects the most current local HEAD' ); }); test('degrade check references #1941 for traceability', () => { const content = fs.readFileSync(QUICK_WORKFLOW_PATH, 'utf-8'); assert.ok(content.includes('#1941'), 'quick.md must reference #1941'); }); test('degrade check clears BOTH USE_WORKTREES and ISOLATION when shouldDegrade is true', () => { const content = fs.readFileSync(QUICK_WORKFLOW_PATH, 'utf-8'); const baseCheckIdx = content.indexOf('worktree.base-check'); const block = content.slice(baseCheckIdx, baseCheckIdx + 900); assert.ok(block.includes('shouldDegrade'), 'degrade check must branch on shouldDegrade'); // Both must move together (#2652). Dispatch keys on ISOLATION while the prompt // guard and worktree manifest key on USE_WORKTREES; clearing only one dispatches // an isolated executor with no base guard and no manifest, then blocks in cleanup // looking for a manifest that was never initialized. assert.ok(block.includes('USE_WORKTREES=false'), 'degrade must set USE_WORKTREES=false'); assert.ok( block.includes('ISOLATION=none'), 'degrade must ALSO set ISOLATION=none — dispatch reads ISOLATION, so clearing only ' + 'USE_WORKTREES still passes the harness isolation flag (#2652)' ); }); // #2652: this assertion previously required `RUNTIME = "claude"`, encoding the // pre-#2584 premise that worktree isolation is Claude-specific. #2584 replaced // that with the negotiated dispatch.isolation capability, so the guard now keys // on the capability — Cursor also declares harness-worktree. test('degrade check guards on the negotiated capability, not a runtime id', () => { const content = fs.readFileSync(QUICK_WORKFLOW_PATH, 'utf-8'); const baseCheckIdx = content.indexOf('worktree.base-check'); const block = content.slice(Math.max(0, baseCheckIdx - 300), baseCheckIdx + 200); assert.ok( block.includes('ISOLATION') && block.includes('harness-worktree'), 'degrade check must guard on ISOLATION = harness-worktree' ); assert.ok( !/\[\s*"\$RUNTIME"\s*=/.test(block), 'degrade check must NOT branch on a RUNTIME literal (#2584/#2652)' ); }); test('degrade check names origin/HEAD as the stale fork base', () => { const content = fs.readFileSync(QUICK_WORKFLOW_PATH, 'utf-8'); const step6Idx = content.indexOf('**Step 6: Spawn executor**'); const nextSection = content.indexOf('\n---', step6Idx); const section = content.slice(step6Idx, nextSection === -1 ? undefined : nextSection); assert.ok(section.includes('origin/HEAD'), 'Step 6 must name origin/HEAD as the stale fork base'); }); }); }); } { const { describe: __foldDescribe } = require('node:test'); __foldDescribe('folded:fix-2649-diagnose-issues-worktree-stale-base', () => { const DIAGNOSE_ISSUES_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'diagnose-issues.md'); const EXECUTE_PLAN_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-plan.md'); describe('diagnose-issues: pre-dispatch worktree base-check (#2649)', () => { test('workflow file exists', () => { assert.ok(fs.existsSync(DIAGNOSE_ISSUES_PATH), 'workflows/diagnose-issues.md should exist'); }); test('spawn_agents step runs worktree.base-check before the Agent() dispatch', () => { const content = fs.readFileSync(DIAGNOSE_ISSUES_PATH, 'utf-8'); const spawnIdx = content.indexOf(''); assert.ok(spawnIdx !== -1, '"spawn_agents" step must exist in diagnose-issues.md'); const baseCheckIdx = content.indexOf('worktree.base-check', spawnIdx); assert.ok(baseCheckIdx !== -1, 'worktree.base-check must be invoked within the spawn_agents step'); // The load-bearing invariant is "base-check BEFORE the Agent() dispatch" so the // degrade decision can drop isolation from the spawn. (Where EXPECTED_BASE is // captured relative to the check is cosmetic — the check only reads HEAD, never // mutates it — so assert the real invariant, not a loose disjunction.) const agentIdx = content.indexOf('Agent(', spawnIdx); assert.ok(agentIdx !== -1, 'spawn_agents must contain an Agent() dispatch'); assert.ok( baseCheckIdx < agentIdx, 'worktree.base-check must run before the Agent() dispatch so the degrade decision can drop isolation from the spawn', ); }); test('verify-only worktree_branch_check backstop remains embedded in the Agent() prompt', () => { // Acceptance criterion #4: the base-check is a PRE-DISPATCH degrade; the // guard is a POST-FORK fail-closed backstop. Both // layers must survive — a future edit that dropped the backstop embedding // would re-open the silent-stale-base class. Guard its continued presence. const content = fs.readFileSync(DIAGNOSE_ISSUES_PATH, 'utf-8'); const spawnIdx = content.indexOf(''); assert.ok(spawnIdx !== -1, '"spawn_agents" step must exist'); assert.ok( content.indexOf('worktree-branch-check.md', spawnIdx) !== -1, 'spawn_agents must still materialize the backstop after the base-check gate (#2649 acceptance criterion 4)', ); }); test('degrade check sets USE_WORKTREES=false when shouldDegrade is true', () => { const content = fs.readFileSync(DIAGNOSE_ISSUES_PATH, 'utf-8'); const baseCheckIdx = content.indexOf('worktree.base-check'); const block = content.slice(baseCheckIdx, baseCheckIdx + 600); assert.ok( block.includes('shouldDegrade') && block.includes('USE_WORKTREES=false'), 'degrade check must override USE_WORKTREES=false when shouldDegrade is true', ); }); test('degrade check references #2649 for traceability', () => { const content = fs.readFileSync(DIAGNOSE_ISSUES_PATH, 'utf-8'); assert.ok(content.includes('#2649'), 'diagnose-issues.md must reference #2649'); }); }); describe('execute-plan Pattern A: pre-dispatch worktree base-check (#2649)', () => { test('workflow file exists', () => { assert.ok(fs.existsSync(EXECUTE_PLAN_PATH), 'workflows/execute-plan.md should exist'); }); test('Pattern A runs the worktree base-check before spawning the executor', () => { const content = fs.readFileSync(EXECUTE_PLAN_PATH, 'utf-8'); const patternAIdx = content.indexOf('**Pattern A:**'); assert.ok(patternAIdx !== -1, '"Pattern A:" must exist in execute-plan.md'); // The base-check instruction must appear within the Pattern A description, // before the isolation="worktree" embedding instruction. const patternAEnd = content.indexOf('**Pattern B:**', patternAIdx); const patternA = content.slice(patternAIdx, patternAEnd === -1 ? undefined : patternAEnd); assert.ok( patternA.includes('#2649') && /worktree\.base-check|base-check/.test(patternA), 'Pattern A must run the #2649 worktree base-check before dispatching the executor', ); assert.ok( patternA.includes('shouldDegrade'), 'Pattern A base-check must consult shouldDegrade', ); }); test('Pattern A documents the auto-degrade (drop isolation on shouldDegrade)', () => { const content = fs.readFileSync(EXECUTE_PLAN_PATH, 'utf-8'); const patternAIdx = content.indexOf('**Pattern A:**'); const patternAEnd = content.indexOf('**Pattern B:**', patternAIdx); const patternA = content.slice(patternAIdx, patternAEnd === -1 ? undefined : patternAEnd); assert.ok( /degrad|sequential/i.test(patternA), 'Pattern A must document auto-degrading to sequential mode when shouldDegrade is true', ); }); }); }); } describe('#4734: classifyGitHead — single owner of the HEAD-resolution classes', () => { test('exit 0 with a sha → present, headSha carries the trimmed sha', () => { const SHA = 'aabbccdd11223344aabbccdd11223344aabbccdd'; const status = classifyGitHead({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: `${SHA}\n`, stderr: '', signal: null, error: null }, }), }); assert.strictEqual(status.status, 'present'); assert.strictEqual(status.headSha, SHA); }); test('exit 128 → definitive-absence (not a repository, or a repository with no commits — neither can host a worktree)', () => { const status = classifyGitHead({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository', signal: null, error: null }, }), }); assert.strictEqual(status.status, 'definitive-absence'); assert.strictEqual(status.headSha, null); }); test('a REAL non-git working directory degrades end-to-end (no injected seam — the #4734 fixture wording)', (t) => { const { createTempDir, cleanup } = require('./helpers.cjs'); const dir = createTempDir('gsd-4734-nogit-real-'); t.after(() => cleanup(dir)); const result = evaluateWorktreeBaseDegrade({ cwd: dir }); assert.strictEqual(result.shouldDegrade, true); assert.strictEqual(result.reason, 'no-head'); assert.strictEqual(result.headAbsenceVerified, true); assert.ok(typeof result.message === 'string' && result.message.length > 0); }); test('exit 0 with empty stdout → ambiguous-absence (git completed without a definitive answer)', () => { const status = classifyGitHead({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 0, stdout: '', stderr: '', signal: null, error: null }, }), }); assert.strictEqual(status.status, 'ambiguous-absence'); assert.strictEqual(status.headSha, null); }); test('timeout → indeterminate (fail closed)', () => { const timedOutErr = new Error('spawnSync git ETIMEDOUT'); timedOutErr.code = 'ETIMEDOUT'; const status = classifyGitHead({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: timedOutErr }, }), }); assert.strictEqual(status.status, 'indeterminate'); assert.strictEqual(status.headSha, null); }); test('other non-zero exit (git missing, exit 127) → indeterminate (fail closed)', () => { const status = classifyGitHead({ execGit: makeExecGit({ 'rev-parse HEAD': { exitCode: 127, stdout: '', stderr: 'command not found', signal: null, error: null }, }), }); assert.strictEqual(status.status, 'indeterminate'); 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'); }); });