Files
msd-core/tests/fix-1941-quick-worktree-stale-base.test.cjs
Joe Slitzker 3fe9a81428 fix(#1941): degrade /gsd-quick worktree dispatch when fork base is stale
Claude Code's isolation="worktree" forks new worktrees from origin/HEAD, not
the live local HEAD. When prior local commits (e.g. an earlier quick task in
the same session, or this task's own Step 5.6 pre-dispatch plan commit)
advance local HEAD without an intervening push, origin/HEAD stays pinned to a
stale ancestor and the executor's worktree_branch_check guard halts with a
base-mismatch fatal that can be many commits behind, not just one.

Port the worktree.base-check auto-degrade pattern already used by
execute-phase (#683/#1369) into quick.md's single-dispatch path, run
immediately before EXPECTED_BASE is captured in Step 6.
2026-07-06 13:01:47 -05:00

77 lines
3.8 KiB
JavaScript

// allow-test-rule: source-text-is-the-product #1941
// 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.
/**
* Regression tests for bug #1941: /gsd-quick worktree executor forks from a stale base —
* up to many commits behind, not just the one-commit gap #1265 already covers.
*
* Root cause: Claude Code's isolation="worktree" forks new worktrees from origin/HEAD, not
* the live local HEAD. When prior local commits (e.g. earlier quick tasks in the same
* session, or this task's own Step 5.6 pre-dispatch plan commit) 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 already used by execute-phase (#683/#1369) into
* quick.md's single-dispatch path.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const 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(WORKFLOW_PATH), 'workflows/quick.md should exist');
});
test('Step 6 runs worktree.base-check before capturing EXPECTED_BASE', () => {
const content = fs.readFileSync(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(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('#1941'), 'quick.md must reference #1941');
});
test('degrade check sets USE_WORKTREES=false when shouldDegrade is true', () => {
const content = fs.readFileSync(WORKFLOW_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 guards on RUNTIME=claude (worktree isolation is Claude Code-specific)', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
const baseCheckIdx = content.indexOf('worktree.base-check');
const block = content.slice(Math.max(0, baseCheckIdx - 200), baseCheckIdx + 200);
assert.ok(
block.includes('RUNTIME') && (block.includes('"claude"') || block.includes("'claude'")),
'degrade check must guard on RUNTIME=claude'
);
});
test('degrade check names origin/HEAD as the stale fork base', () => {
const content = fs.readFileSync(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');
});
});