test: deepen fixture module v2 for reliability (#329)

* test: deepen fixture seam for init/state/workstream suites

* test: deepen fixture module v2 with declarative builders
This commit is contained in:
Tom Boucher
2026-05-26 11:23:42 -04:00
committed by GitHub
parent 656bff8b73
commit 06cf7826e3
6 changed files with 304 additions and 132 deletions

View File

@@ -0,0 +1,86 @@
const { test, describe, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { createFixture, seedPhase, seedWorkstream, writeState } = require('./fixtures/index.cjs');
const { cleanup } = require('./helpers.cjs');
const created = [];
afterEach(() => {
while (created.length > 0) cleanup(created.pop());
});
describe('fixture builder module', () => {
test('creates canonical planning layout by default', () => {
const dir = createFixture();
created.push(dir);
assert.ok(fs.existsSync(path.join(dir, '.planning')), 'creates .planning directory');
assert.ok(fs.existsSync(path.join(dir, '.planning', 'phases')), 'creates .planning/phases directory');
});
test('seeds phase files declaratively', () => {
const dir = createFixture();
created.push(dir);
const phaseDir = seedPhase(dir, '03-api', {
'03-01-PLAN.md': '# Plan',
'03-CONTEXT.md': '# Context',
});
assert.ok(fs.existsSync(path.join(phaseDir, '03-01-PLAN.md')));
assert.ok(fs.existsSync(path.join(phaseDir, '03-CONTEXT.md')));
});
test('seeds workstream and active pointer declaratively', () => {
const dir = createFixture();
created.push(dir);
const wsDir = seedWorkstream(dir, {
name: 'alpha',
state: '# State\n',
roadmap: '# Roadmap\n',
active: true,
});
assert.ok(fs.existsSync(path.join(wsDir, 'STATE.md')));
assert.ok(fs.existsSync(path.join(wsDir, 'ROADMAP.md')));
assert.strictEqual(
fs.readFileSync(path.join(dir, '.planning', 'active-workstream'), 'utf8').trim(),
'alpha'
);
});
test('rejects invalid workstream names', () => {
const dir = createFixture();
created.push(dir);
assert.throws(() => seedWorkstream(dir, { name: '../escape' }), /invalid name/);
});
test('writes STATE.md in canonical location', () => {
const dir = createFixture();
created.push(dir);
const p = writeState(dir, '# Project State\n');
assert.ok(fs.existsSync(p));
assert.strictEqual(path.basename(p), 'STATE.md');
});
test('initializes git fixture with initial commit', () => {
const dir = createFixture({ git: true });
created.push(dir);
const isWorkTree = execSync('git rev-parse --is-inside-work-tree', { cwd: dir, encoding: 'utf8' }).trim();
assert.strictEqual(isWorkTree, 'true', 'fixture should be a git worktree');
const head = execSync('git rev-parse HEAD', { cwd: dir, encoding: 'utf8' }).trim();
assert.ok(head.length > 0, 'fixture should include initial commit');
assert.ok(
fs.existsSync(path.join(dir, '.planning', 'PROJECT.md')),
'git fixture writes canonical PROJECT.md'
);
});
});

101
tests/fixtures/index.cjs vendored Normal file
View File

@@ -0,0 +1,101 @@
const { execSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
/**
* Create a temp test fixture directory with canonical planning layout.
*
* @param {object} [options]
* @param {string} [options.prefix='gsd-test-']
* @param {boolean} [options.git=false] - initialize git repo with initial commit
* @param {boolean} [options.planning=true] - create .planning/phases layout
* @param {boolean} [options.projectDoc=true] - write .planning/PROJECT.md
* @returns {string} absolute fixture directory path
*/
function createFixture(options = {}) {
const {
prefix = 'gsd-test-',
git = false,
planning = true,
projectDoc = git,
} = options;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
if (planning) {
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
}
if (projectDoc) {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Project\n\nTest project.\n'
);
}
if (git) {
execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config commit.gpgsign false', { cwd: tmpDir, stdio: 'pipe' });
execSync('git add -A', { cwd: tmpDir, stdio: 'pipe' });
execSync('git commit -m "initial commit"', { cwd: tmpDir, stdio: 'pipe' });
}
return tmpDir;
}
/**
* Seed a canonical phase directory with files.
* @param {string} tmpDir
* @param {string} phaseSlug e.g. "03-api"
* @param {Record<string,string>} files map of filename->content
* @returns {string} phase directory path
*/
function seedPhase(tmpDir, phaseSlug, files = {}) {
const phaseDir = path.join(tmpDir, '.planning', 'phases', phaseSlug);
fs.mkdirSync(phaseDir, { recursive: true });
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(phaseDir, name), content);
}
return phaseDir;
}
/**
* Seed a canonical workstream tree and optionally set active pointer.
* @param {string} tmpDir
* @param {{name:string, state?:string, roadmap?:string, active?:boolean}} options
* @returns {string} workstream directory path
*/
function seedWorkstream(tmpDir, options) {
const { name, state = '', roadmap = '', active = false } = options || {};
if (!name || /[^a-zA-Z0-9._-]/.test(name) || name.includes('..')) {
throw new Error(`seedWorkstream: invalid name "${name}"`);
}
const wsDir = path.join(tmpDir, '.planning', 'workstreams', name);
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
if (state) fs.writeFileSync(path.join(wsDir, 'STATE.md'), state);
if (roadmap) fs.writeFileSync(path.join(wsDir, 'ROADMAP.md'), roadmap);
if (active) {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.planning', 'active-workstream'), `${name}\n`);
}
return wsDir;
}
/**
* Write STATE.md in canonical location.
* @param {string} tmpDir
* @param {string} content
* @returns {string} file path
*/
function writeState(tmpDir, content) {
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
fs.mkdirSync(path.dirname(statePath), { recursive: true });
fs.writeFileSync(statePath, content);
return statePath;
}
module.exports = { createFixture, seedPhase, seedWorkstream, writeState };

View File

@@ -2,9 +2,10 @@
* GSD Tools Test Helpers
*/
const { execSync, execFileSync } = require('child_process');
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { createFixture } = require('./fixtures/index.cjs');
const TOOLS_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
const TEST_ENV_BASE = {
@@ -86,30 +87,12 @@ function createTempDir(prefix = 'gsd-test-') {
// Create temp directory structure
function createTempProject(prefix = 'gsd-test-') {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), prefix));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
return tmpDir;
return createFixture({ prefix, planning: true, git: false });
}
// Create temp directory with initialized git repo and at least one commit
function createTempGitProject(prefix = 'gsd-test-') {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), prefix));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'pipe' });
execSync('git config commit.gpgsign false', { cwd: tmpDir, stdio: 'pipe' });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Project\n\nTest project.\n'
);
execSync('git add -A', { cwd: tmpDir, stdio: 'pipe' });
execSync('git commit -m "initial commit"', { cwd: tmpDir, stdio: 'pipe' });
return tmpDir;
return createFixture({ prefix, planning: true, git: true, projectDoc: true });
}
function cleanup(tmpDir) {

View File

@@ -6,13 +6,14 @@ const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { runGsdTools, cleanup } = require('./helpers.cjs');
const { createFixture, seedPhase } = require('./fixtures/index.cjs');
describe('init commands', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -20,9 +21,9 @@ describe('init commands', () => {
});
test('init execute-phase returns file paths', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '03-api', {
'03-01-PLAN.md': '# Plan',
});
const result = runGsdTools('init execute-phase 03', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -34,9 +35,9 @@ describe('init commands', () => {
});
test('init execute-phase respects model_overrides for executor_model', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '01-foundation', {
'01-01-PLAN.md': '# Plan',
});
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
model_profile: 'balanced',
model_overrides: { 'gsd-executor': 'openai/o4-mini' },
@@ -51,9 +52,9 @@ describe('init commands', () => {
});
test('init execute-phase respects model_overrides when resolve_model_ids is omit', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '01-foundation', {
'01-01-PLAN.md': '# Plan',
});
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
resolve_model_ids: 'omit',
model_overrides: { 'gsd-executor': 'openai/o4-mini' },
@@ -68,12 +69,12 @@ describe('init commands', () => {
});
test('init plan-phase returns file paths', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification');
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT');
seedPhase(tmpDir, '03-api', {
'03-CONTEXT.md': '# Phase Context',
'03-RESEARCH.md': '# Research Findings',
'03-VERIFICATION.md': '# Verification',
'03-UAT.md': '# UAT',
});
const result = runGsdTools('init plan-phase 03', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -121,12 +122,12 @@ describe('init commands', () => {
});
test('init phase-op returns core and optional phase file paths', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research');
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification');
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT');
seedPhase(tmpDir, '03-api', {
'03-CONTEXT.md': '# Phase Context',
'03-RESEARCH.md': '# Research',
'03-VERIFICATION.md': '# Verification',
'03-UAT.md': '# UAT',
});
const result = runGsdTools('init phase-op 03', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -142,9 +143,9 @@ describe('init commands', () => {
});
test('init plan-phase detects has_reviews and reviews_path when REVIEWS.md exists', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-REVIEWS.md'), '# Cross-AI Reviews');
seedPhase(tmpDir, '03-api', {
'03-REVIEWS.md': '# Cross-AI Reviews',
});
const result = runGsdTools('init plan-phase 03', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -155,8 +156,7 @@ describe('init commands', () => {
});
test('init plan-phase omits optional paths if files missing', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
seedPhase(tmpDir, '03-api');
const result = runGsdTools('init plan-phase 03', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -223,9 +223,9 @@ describe('init commands', () => {
});
test('init execute-phase extracts phase_req_ids from ROADMAP', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '03-api', {
'03-01-PLAN.md': '# Plan',
});
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Requirements**: EX-01, EX-02\n**Plans:** 1 plans\n`
@@ -287,9 +287,9 @@ describe('init commands', () => {
});
test(`init execute-phase parses Requirements with ${variant.name}`, () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '03-api', {
'03-01-PLAN.md': '# Plan',
});
const roadmap = [
'# Roadmap',
'',
@@ -311,9 +311,9 @@ describe('init commands', () => {
}
test('init execute-phase returns null phase_req_ids when Requirements line is absent', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '03-api', {
'03-01-PLAN.md': '# Plan',
});
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Plans:** 1 plans\n`
@@ -335,7 +335,7 @@ describe('init commands ROADMAP fallback when phase directory does not exist (#1
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 1: Foundation Setup\n**Goal:** Bootstrap project\n**Requirements**: R-01, R-02\n**Plans:** TBD\n'
@@ -395,9 +395,9 @@ describe('init commands ROADMAP fallback when phase directory does not exist (#1
});
test('init plan-phase prefers disk directory over ROADMAP fallback', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '01-foundation-setup', {
'01-01-PLAN.md': '# Plan',
});
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -418,7 +418,7 @@ describe('init commands ignore archived phases from prior milestones sharing a n
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
// Current milestone ROADMAP has Phase 2 but no disk directory yet
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
@@ -488,7 +488,7 @@ describe('init plan-phase zero-padded phase number (bug #2391)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
// Current milestone ROADMAP has Phase 3 (unpadded heading)
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
@@ -548,7 +548,7 @@ describe('cmdInitTodos', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -674,7 +674,7 @@ describe('cmdInitMilestoneOp', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -771,7 +771,7 @@ describe('cmdInitPhaseOp fallback', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -779,10 +779,10 @@ describe('cmdInitPhaseOp fallback', () => {
});
test('normal path with existing directory', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Context');
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
seedPhase(tmpDir, '03-api', {
'03-CONTEXT.md': '# Context',
'03-01-PLAN.md': '# Plan',
});
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Plans:** 1 plans\n'
@@ -884,7 +884,7 @@ describe('cmdInitProgress', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1008,7 +1008,7 @@ describe('cmdInitQuick', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1126,7 +1126,7 @@ describe('cmdInitMapCodebase', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1194,7 +1194,7 @@ describe('cmdInitNewProject', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1335,7 +1335,7 @@ describe('cmdInitNewMilestone', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1420,7 +1420,7 @@ describe('findProjectRoot integration via --cwd', () => {
let projectRoot;
beforeEach(() => {
projectRoot = createTempProject();
projectRoot = createFixture();
// Add ROADMAP.md so init quick doesn't error
fs.writeFileSync(
path.join(projectRoot, '.planning', 'ROADMAP.md'),
@@ -1485,7 +1485,7 @@ describe('#2192: init plan-phase includes auto-advance config to prevent separat
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-auth'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
@@ -1552,7 +1552,7 @@ describe('withProjectRoot project identity', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1610,7 +1610,7 @@ describe('withProjectRoot project identity', () => {
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ project_code: 'CK' })
);
// Ensure no PROJECT.md exists (createTempProject doesn't create one)
// Ensure no PROJECT.md exists (createFixture doesn't create one)
const projectMdPath = path.join(tmpDir, '.planning', 'PROJECT.md');
if (fs.existsSync(projectMdPath)) fs.unlinkSync(projectMdPath);

View File

@@ -10,13 +10,14 @@ const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { runGsdTools, cleanup } = require('./helpers.cjs');
const { createFixture } = require('./fixtures/index.cjs');
describe('state-snapshot command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -197,7 +198,7 @@ describe('state-snapshot — bug #3265 frontmatter precedence', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -287,7 +288,7 @@ describe('state mutation commands', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -408,7 +409,7 @@ describe('state json command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -513,7 +514,7 @@ describe('STATE.md frontmatter sync', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -759,7 +760,7 @@ describe('cmdStateLoad (state load)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -821,7 +822,7 @@ describe('cmdStateGet (state get)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -903,7 +904,7 @@ describe('cmdStatePatch and cmdStateUpdate (state patch, state update)', () => {
].join('\n') + '\n';
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -990,7 +991,7 @@ describe('cmdStateAdvancePlan (state advance-plan)', () => {
].join('\n') + '\n';
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1115,7 +1116,7 @@ describe('cmdStateRecordMetric (state record-metric)', () => {
].join('\n') + '\n';
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1186,7 +1187,7 @@ describe('cmdStateUpdateProgress (state update-progress)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1280,7 +1281,7 @@ describe('cmdStateResolveBlocker (state resolve-blocker)', () => {
].join('\n') + '\n';
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1374,7 +1375,7 @@ describe('cmdStateRecordSession (state record-session)', () => {
].join('\n') + '\n';
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1462,7 +1463,7 @@ describe('milestone-scoped phase counting in frontmatter', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1586,7 +1587,7 @@ describe('state begin-phase preserves Current Position fields (#1365)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1710,7 +1711,7 @@ describe('progress counters correct after plan execution (#1589)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -1843,7 +1844,7 @@ describe('updatePerformanceMetricsSection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2013,7 +2014,7 @@ describe('state planned-phase command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2077,7 +2078,7 @@ describe('state validate command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2161,7 +2162,7 @@ describe('state sync command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2252,7 +2253,7 @@ describe('stopped_at frontmatter not overwritten by historical prose (bug #2444)
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2351,7 +2352,7 @@ describe('stale phase dirs do not corrupt phase counts (bug #2445)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {
@@ -2480,7 +2481,7 @@ describe('state complete-phase: decorated Phase fallback (#2761 nitpick)', () =>
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
});
afterEach(() => {

View File

@@ -8,7 +8,8 @@ const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { runGsdTools, cleanup } = require('./helpers.cjs');
const { createFixture, seedWorkstream, writeState } = require('./fixtures/index.cjs');
const { migrateToWorkstreams, getOtherActiveWorkstreams } = require('../get-shit-done/bin/lib/workstream.cjs');
// ─── Helper ──────────────────────────────────────────────────────────────────
@@ -18,7 +19,7 @@ function createProjectWithState(tmpDir, roadmap, state) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmap, 'utf-8');
}
if (state) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), state, 'utf-8');
writeState(tmpDir, state);
}
}
@@ -75,13 +76,13 @@ describe('planningDir workstream awareness via env var', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
// Create workstream structure
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'alpha');
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'STATE.md'), '# State\n**Status:** In progress\n**Current Phase:** 1\n');
fs.writeFileSync(path.join(wsDir, 'ROADMAP.md'), '## Roadmap v1.0: Alpha\n### Phase 1: Setup\n');
fs.writeFileSync(path.join(tmpDir, '.planning', 'active-workstream'), 'alpha\n');
tmpDir = createFixture();
seedWorkstream(tmpDir, {
name: 'alpha',
state: '# State\n**Status:** In progress\n**Current Phase:** 1\n',
roadmap: '## Roadmap v1.0: Alpha\n### Phase 1: Setup\n',
active: true,
});
});
after(() => cleanup(tmpDir));
@@ -119,7 +120,7 @@ describe('session-scoped active workstream routing', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
for (const [ws, status] of [['alpha', 'Alpha active'], ['beta', 'Beta active']]) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
@@ -193,7 +194,7 @@ describe('session resolution hardening', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
for (const [ws, status] of [['alpha', 'Alpha active'], ['beta', 'Beta active']]) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
@@ -254,7 +255,7 @@ describe('pointer lifecycle hardening', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
for (const [ws, status] of [['alpha', 'Alpha active'], ['beta', 'Beta active']]) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
@@ -323,7 +324,7 @@ describe('workstream create', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n');
});
@@ -365,7 +366,7 @@ describe('workstream create with migration', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n');
// Existing flat-mode work
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '## Roadmap v1.0: Existing\n### Phase 1: A\n');
@@ -389,7 +390,7 @@ describe('workstream create with migration', () => {
});
test('normalizes --migrate-name to a valid workstream slug', () => {
const isolatedDir = createTempProject();
const isolatedDir = createFixture();
try {
fs.writeFileSync(path.join(isolatedDir, '.planning', 'PROJECT.md'), '# Project\n');
fs.writeFileSync(path.join(isolatedDir, '.planning', 'ROADMAP.md'), '## Roadmap v1.0: Existing\n### Phase 1: A\n');
@@ -414,7 +415,7 @@ describe('workstream create with migration', () => {
describe('migrateToWorkstreams', () => {
test('rejects invalid workstream names for migration', () => {
const tmpDir = createTempProject();
const tmpDir = createFixture();
try {
assert.throws(
() => migrateToWorkstreams(tmpDir, 'bad/name'),
@@ -426,7 +427,7 @@ describe('migrateToWorkstreams', () => {
});
test('fails when already in workstream mode', () => {
const tmpDir = createTempProject();
const tmpDir = createFixture();
try {
fs.mkdirSync(path.join(tmpDir, '.planning', 'workstreams', 'existing'), { recursive: true });
assert.throws(
@@ -443,7 +444,7 @@ describe('workstream list', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
// Create two workstreams
for (const ws of ['alpha', 'beta']) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
@@ -468,7 +469,7 @@ describe('workstream list', () => {
let flatDir;
beforeEach(() => {
flatDir = createTempProject();
flatDir = createFixture();
});
afterEach(() => {
@@ -488,7 +489,7 @@ describe('workstream status', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'alpha');
fs.mkdirSync(path.join(wsDir, 'phases', '01-setup'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'phases', '01-setup', 'PLAN.md'), '# Plan\n');
@@ -521,7 +522,7 @@ describe('workstream complete', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'done-ws');
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'STATE.md'), '# State\n**Status:** Complete\n');
@@ -549,7 +550,7 @@ describe('workstream set/get', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
for (const ws of ['ws-a', 'ws-b']) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
@@ -596,7 +597,7 @@ describe('getOtherActiveWorkstreams', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
// Create 3 workstreams: alpha (active), beta (active), gamma (completed)
for (const ws of ['alpha', 'beta', 'gamma']) {
const wsDir = path.join(tmpDir, '.planning', 'workstreams', ws);
@@ -643,7 +644,7 @@ describe('workstream progress', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'feature');
fs.mkdirSync(path.join(wsDir, 'phases', '01-init'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'phases', '01-init', 'PLAN.md'), '# Plan\n');
@@ -667,7 +668,7 @@ describe('workstream progress', () => {
});
test('clamps progress percent when completed phase dirs exceed roadmap count', () => {
const isolatedDir = createTempProject();
const isolatedDir = createFixture();
try {
const wsDir = path.join(isolatedDir, '.planning', 'workstreams', 'overflow');
for (const phase of ['01-one', '02-two']) {
@@ -689,7 +690,7 @@ describe('workstream progress', () => {
});
test('returns flat mode when no workstreams exist', () => {
const emptyDir = createTempProject();
const emptyDir = createFixture();
try {
const result = runGsdTools(['workstream', 'progress', '--raw'], emptyDir);
assert.ok(result.success, `progress in flat mode failed: ${result.error}`);
@@ -707,7 +708,7 @@ describe('gsd-tools --ws flag integration', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
// Create a workstream with roadmap
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'test-ws');
fs.mkdirSync(path.join(wsDir, 'phases', '01-setup'), { recursive: true });
@@ -741,7 +742,7 @@ describe('path traversal rejection', () => {
let tmpDir;
before(() => {
tmpDir = createTempProject();
tmpDir = createFixture();
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n');
const wsDir = path.join(tmpDir, '.planning', 'workstreams', 'legit');
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });