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:
86
tests/fixture-builder.test.cjs
Normal file
86
tests/fixture-builder.test.cjs
Normal 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
101
tests/fixtures/index.cjs
vendored
Normal 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 };
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user