Merge pull request #1320 from gsd-build/fix/workstream-post-merge-cleanup

fix: address post-merge review concerns from #1268
This commit is contained in:
Tom Boucher
2026-03-22 10:54:33 -04:00
committed by GitHub
3 changed files with 24 additions and 9 deletions

View File

@@ -589,6 +589,9 @@ function setActiveWorkstream(cwd, name) {
try { fs.unlinkSync(filePath); } catch {}
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error('Invalid workstream name: must be alphanumeric, hyphens, and underscores only');
}
fs.writeFileSync(filePath, name + '\n', 'utf-8');
}

View File

@@ -617,10 +617,10 @@ describe('copyCommandsAsCopilotSkills', () => {
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-help')), 'gsd-help folder exists');
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-progress')), 'gsd-progress folder exists');
// Count gsd-* directories — should be 31
// Count gsd-* directories — should be 57
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
assert.strictEqual(dirs.length, 56, `expected 56 skill folders, got ${dirs.length}`);
assert.strictEqual(dirs.length, 57, `expected 57 skill folders, got ${dirs.length}`);
} finally {
fs.rmSync(tempDir, { recursive: true });
}
@@ -1114,7 +1114,7 @@ const { execFileSync } = require('child_process');
const crypto = require('crypto');
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
const EXPECTED_SKILLS = 56;
const EXPECTED_SKILLS = 57;
const EXPECTED_AGENTS = 18;
function runCopilotInstall(cwd) {

View File

@@ -408,12 +408,11 @@ describe('path traversal rejection', () => {
for (const name of maliciousNames) {
test(`rejects set ${name}`, () => {
const result = runGsdTools(['workstream', 'set', name, '--raw'], tmpDir);
// set validates independently — should return error or invalid_name
assert.ok(result.success || !result.success, 'should handle gracefully');
if (result.success) {
const data = JSON.parse(result.output);
assert.strictEqual(data.error, 'invalid_name', `should return invalid_name error for: ${name}`);
}
// cmdWorkstreamSet validates the positional arg and returns invalid_name error
assert.ok(result.success, `command should exit cleanly for: ${name}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.error, 'invalid_name', `should return invalid_name error for: ${name}`);
assert.strictEqual(data.active, null, `active should be null for: ${name}`);
});
}
});
@@ -436,4 +435,17 @@ describe('path traversal rejection', () => {
try { fs.unlinkSync(path.join(tmpDir, '.planning', 'active-workstream')); } catch {}
});
});
describe('setActiveWorkstream rejects invalid names directly', () => {
const { setActiveWorkstream } = require('../get-shit-done/bin/lib/core.cjs');
for (const name of maliciousNames) {
test(`throws for ${name}`, () => {
assert.throws(
() => setActiveWorkstream(tmpDir, name),
{ message: /Invalid workstream name/ },
`should throw for: ${name}`
);
});
}
});
});