Merge pull request #1338 from gsd-build/fix/worktree-edit-permissions
fix: add permissionMode to worktree agents
This commit is contained in:
@@ -339,3 +339,56 @@ describe('DISCUSS: discussion log generation', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Worktree Permission Mode (#1334) ───────────────────────────────────────
|
||||
|
||||
describe('PERM: worktree agents have permissionMode: acceptEdits', () => {
|
||||
// Agents spawned with isolation="worktree" need permissionMode: acceptEdits
|
||||
// to avoid per-directory edit permission prompts in the worktree path.
|
||||
// See: anthropics/claude-code#29110, anthropics/claude-code#28041
|
||||
const WORKTREE_AGENTS = ['gsd-executor', 'gsd-debugger'];
|
||||
|
||||
for (const agent of WORKTREE_AGENTS) {
|
||||
test(`${agent} has permissionMode: acceptEdits`, () => {
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8');
|
||||
const frontmatter = content.split('---')[1] || '';
|
||||
assert.ok(
|
||||
frontmatter.includes('permissionMode: acceptEdits'),
|
||||
`${agent} must have permissionMode: acceptEdits — worktree agents need this to avoid ` +
|
||||
`per-directory edit permission prompts (see #1334)`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('worktree-spawned agents are covered', () => {
|
||||
// Verify that agents referenced with isolation="worktree" in workflows
|
||||
// are included in the WORKTREE_AGENTS list above
|
||||
const dirs = [WORKFLOWS_DIR, COMMANDS_DIR];
|
||||
const worktreeAgentTypes = new Set();
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
||||
// Find patterns like: subagent_type="gsd-executor" ... isolation="worktree"
|
||||
// These can span multiple lines in Task() calls
|
||||
const taskBlocks = content.match(/Task\([^)]*isolation="worktree"[^)]*\)/gs) || [];
|
||||
for (const block of taskBlocks) {
|
||||
const typeMatch = block.match(/subagent_type="([^"]+)"/);
|
||||
if (typeMatch) {
|
||||
worktreeAgentTypes.add(typeMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const agentType of worktreeAgentTypes) {
|
||||
assert.ok(
|
||||
WORKTREE_AGENTS.includes(agentType),
|
||||
`${agentType} is spawned with isolation="worktree" but not in WORKTREE_AGENTS list — ` +
|
||||
`add permissionMode: acceptEdits to its frontmatter and update this test`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +136,12 @@ describe('dispatcher error paths', () => {
|
||||
assert.ok(result.error.includes('Unknown todo subcommand'), `Expected "Unknown todo subcommand" in stderr, got: ${result.error}`);
|
||||
});
|
||||
|
||||
test('uat unknown subcommand errors', () => {
|
||||
const result = runGsdTools('uat bogus', tmpDir);
|
||||
assert.strictEqual(result.success, false, 'Should exit non-zero');
|
||||
assert.ok(result.error.includes('Unknown uat subcommand'), `Expected "Unknown uat subcommand" in stderr, got: ${result.error}`);
|
||||
});
|
||||
|
||||
// Unknown subcommand: init
|
||||
test('init unknown workflow errors', () => {
|
||||
const result = runGsdTools('init bogus', tmpDir);
|
||||
|
||||
@@ -951,16 +951,15 @@ describe('cmdInitMapCodebase', () => {
|
||||
assert.strictEqual(output.codebase_dir_exists, true);
|
||||
});
|
||||
|
||||
test('map-codebase workflow lists OpenCode as having Task tool support (#1316)', () => {
|
||||
test('map-codebase workflow does not list OpenCode under runtimes without Task tool (#1316)', () => {
|
||||
const workflow = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'workflows', 'map-codebase.md'), 'utf8'
|
||||
);
|
||||
// OpenCode must appear in the "with Task tool" line, not the "WITHOUT" line
|
||||
const withLine = workflow.split('\n').find(l => l.includes('Runtimes with Task tool'));
|
||||
const withoutLine = workflow.split('\n').find(l => l.includes('WITHOUT Task tool'));
|
||||
assert.ok(withLine, 'workflow should have a "Runtimes with Task tool" line');
|
||||
assert.ok(withoutLine, 'workflow should have a "WITHOUT Task tool" line');
|
||||
assert.ok(withLine.includes('OpenCode'), 'OpenCode must be listed under runtimes WITH Task tool');
|
||||
// OpenCode must NOT appear in the "WITHOUT Task tool" / "NOT available" condition
|
||||
const withoutLine = workflow.split('\n').find(l =>
|
||||
l.includes('NOT available') || l.includes('WITHOUT Task tool')
|
||||
);
|
||||
assert.ok(withoutLine, 'workflow should have a line about Task tool NOT being available');
|
||||
assert.ok(!withoutLine.includes('OpenCode'), 'OpenCode must NOT be listed under runtimes WITHOUT Task tool');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
requireSafePath,
|
||||
scanForInjection,
|
||||
sanitizeForPrompt,
|
||||
sanitizeForDisplay,
|
||||
safeJsonParse,
|
||||
validatePhaseNumber,
|
||||
validateFieldName,
|
||||
@@ -244,6 +245,19 @@ describe('sanitizeForPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeForDisplay', () => {
|
||||
test('removes protocol leak lines', () => {
|
||||
const input = 'Visible line\nuser to=all:final code something bad\nAnother line';
|
||||
const result = sanitizeForDisplay(input);
|
||||
assert.equal(result, 'Visible line\nAnother line');
|
||||
});
|
||||
|
||||
test('keeps normal user-facing copy intact', () => {
|
||||
const input = 'Type `pass` or describe what\\\'s wrong.';
|
||||
assert.equal(sanitizeForDisplay(input), input);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Shell Safety ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('validateShellArg', () => {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* GSD Tools Tests - UAT Audit
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
@@ -324,3 +326,83 @@ All checks passed.
|
||||
assert.strictEqual(output.summary.total_files, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uat render-checkpoint', () => {
|
||||
let tmpDir;
|
||||
let uatPath;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test-phase');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
uatPath = path.join(phaseDir, '01-UAT.md');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('renders the current checkpoint as raw output', () => {
|
||||
fs.writeFileSync(uatPath, `---
|
||||
status: testing
|
||||
phase: 01-test-phase
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
number: 2
|
||||
name: Submit form validation
|
||||
expected: |
|
||||
Empty submit keeps controls visible.
|
||||
Validation error copy is shown.
|
||||
awaiting: user response
|
||||
`);
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
assert.ok(result.output.includes('**Test 2: Submit form validation**'));
|
||||
assert.ok(result.output.includes('Empty submit keeps controls visible.'));
|
||||
assert.ok(result.output.includes("Type `pass` or describe what's wrong."));
|
||||
});
|
||||
|
||||
test('strips protocol leak lines from current test copy', () => {
|
||||
fs.writeFileSync(uatPath, `---
|
||||
status: testing
|
||||
phase: 01-test-phase
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
number: 6
|
||||
name: Locale copy
|
||||
expected: |
|
||||
English strings render correctly.
|
||||
user to=all:final code 彩票平台招商 pass
|
||||
Chinese strings render correctly.
|
||||
awaiting: user response
|
||||
`);
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
assert.ok(!result.output.includes('user to=all:final code'));
|
||||
assert.ok(!result.output.includes('彩票平台'));
|
||||
assert.ok(result.output.includes('English strings render correctly.'));
|
||||
assert.ok(result.output.includes('Chinese strings render correctly.'));
|
||||
});
|
||||
|
||||
test('fails when testing is already complete', () => {
|
||||
fs.writeFileSync(uatPath, `---
|
||||
status: complete
|
||||
phase: 01-test-phase
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
[testing complete]
|
||||
`);
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir);
|
||||
assert.strictEqual(result.success, false, 'Should fail when no current test exists');
|
||||
assert.ok(result.error.includes('already complete'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user