* test(#3737): use_worktrees=false opt-out must win in the dispatch-isolation sentinel * fix(#3737): fold the workflow.use_worktrees opt-out into the dispatch-isolation record * test(#3737): guard allows sequential dispatch under the opt-out sentinel * chore(#3737): changeset fragment (pr number backfilled after PR creation) * chore(#3737): backfill changeset PR number (3938) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -937,6 +937,116 @@ describe('#3045 CORE REDESIGN — dispatch-isolation records as an unconditional
|
||||
assert.equal(read.phase, '2');
|
||||
assert.equal(read.plan, 'p1');
|
||||
});
|
||||
|
||||
// #3737 — the project-level opt-out (workflow.use_worktrees === false) is
|
||||
// decided by the workflow shell AFTER the resolve, so pre-fix any plain
|
||||
// re-query re-persisted the naturally-resolved host capability over the
|
||||
// mandated `--force-isolation none` record, and the guard then denied the
|
||||
// sequential dispatch the config explicitly asked for.
|
||||
function writeUseWorktrees(dir, value) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.planning', 'config.json'),
|
||||
JSON.stringify({ runtime: 'claude', workflow: { use_worktrees: value } }),
|
||||
);
|
||||
}
|
||||
|
||||
test('#3737: use_worktrees=false — a plain re-query records none and does not clobber the forced record', (t) => {
|
||||
const dir = createTempProject('gsd-3737-optout-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeUseWorktrees(dir, false);
|
||||
|
||||
const forced = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--raw', '--force-isolation', 'none'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(forced.success, true, forced.error);
|
||||
assert.equal(forced.output.trim(), 'none');
|
||||
|
||||
// The workflow's own re-record step, then the plain re-query that pre-fix
|
||||
// flipped the sentinel back to harness-worktree (#3737 reproduction).
|
||||
const requery = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--raw'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(requery.success, true, requery.error);
|
||||
assert.equal(requery.output.trim(), 'none', '#3737: the opt-out must win on every host');
|
||||
const sentinel = readSentinelRaw(dir);
|
||||
assert.equal(sentinel.isolation, 'none', '#3737: a plain re-query must not re-persist the host capability over the opt-out');
|
||||
assert.equal(sentinel.harness_flag, null);
|
||||
});
|
||||
|
||||
test('#3737: use_worktrees=false resolves and records none on the first plain query (--json agrees)', (t) => {
|
||||
const dir = createTempProject('gsd-3737-optout-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeUseWorktrees(dir, false);
|
||||
|
||||
const result = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--json'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(result.success, true, result.error);
|
||||
const parsed = JSON.parse(result.output);
|
||||
assert.equal(parsed.isolation, 'none');
|
||||
assert.equal(parsed.harnessFlag, null);
|
||||
assert.equal(parsed.exec, null);
|
||||
const sentinel = readSentinelRaw(dir);
|
||||
assert.equal(sentinel.isolation, 'none');
|
||||
assert.equal(sentinel.harness_flag, null);
|
||||
});
|
||||
|
||||
test('#3737: use_worktrees=true keeps the natural harness-worktree record', (t) => {
|
||||
const dir = createTempProject('gsd-3737-optout-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeUseWorktrees(dir, true);
|
||||
|
||||
const result = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--raw'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(result.success, true, result.error);
|
||||
assert.equal(result.output.trim(), 'harness-worktree');
|
||||
assert.equal(readSentinelRaw(dir).isolation, 'harness-worktree');
|
||||
});
|
||||
|
||||
test('#3737: a non-boolean "false" string is not an opt-out (strict === false)', (t) => {
|
||||
const dir = createTempProject('gsd-3737-optout-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeUseWorktrees(dir, 'false');
|
||||
|
||||
const result = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--raw'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(result.success, true, result.error);
|
||||
assert.equal(result.output.trim(), 'harness-worktree', 'a string "false" must degrade to the default (worktrees on), never coerce');
|
||||
assert.equal(readSentinelRaw(dir).isolation, 'harness-worktree');
|
||||
});
|
||||
|
||||
test('#3737: end-to-end — an opted-out project records none and the guard ALLOWS the sequential dispatch', (t) => {
|
||||
const dir = createTempProject('gsd-3737-optout-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeUseWorktrees(dir, false);
|
||||
|
||||
// The workflow's resolve step: a plain query (no force) records the
|
||||
// opt-out decision — the record the issue says must survive re-queries.
|
||||
const result = runGsdTools(
|
||||
['query', 'dispatch-isolation', '--raw'],
|
||||
dir,
|
||||
{ GSD_RUNTIME: 'claude', HOME: dir },
|
||||
);
|
||||
assert.equal(result.success, true, result.error);
|
||||
assert.equal(result.output.trim(), 'none');
|
||||
|
||||
// The guard fires on the sequential inline Agent() dispatch (no
|
||||
// isolation kwarg) and must NOT deny it (#3737's user-visible symptom).
|
||||
const r = runHook(agentPayload(), dir);
|
||||
assert.equal(r.status, 0, `guard denied a sequential dispatch under the opt-out sentinel: stdout=${r.stdout} stderr=${r.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#3045 MAJOR — --harness-flag can now accept a bare CLI-flag value (Cursor real registry value + generalized parsing)', () => {
|
||||
|
||||
Reference in New Issue
Block a user