Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
391 lines
22 KiB
JavaScript
391 lines
22 KiB
JavaScript
// allow-test-rule: source-text-is-the-product see #4455
|
|
// Workflow .md files — their text IS what the runtime loads. Testing text
|
|
// content (as extracted, executed bash fences) tests the deployed contract.
|
|
// Per CONTRIBUTING.md exception matrix.
|
|
|
|
/**
|
|
* MSD Tools Tests - autonomous.md and complete-milestone.md workstream-scoped
|
|
* STATE/ROADMAP/archive paths (#4455)
|
|
*
|
|
* Both workflows used to read/write hardcoded literal `.planning/STATE.md` /
|
|
* `.planning/ROADMAP.md` / `.planning/milestones/...` paths in their shell
|
|
* fences — bypassing workstream scoping entirely. When MSD_WORKSTREAM is set,
|
|
* `init.manager`/`init.complete-milestone` resolve `state_path`/`roadmap_path`/
|
|
* `archive_dir` into the workstream, but the literal reads/writes still hit
|
|
* root `.planning/` (or silently returned empty).
|
|
*
|
|
* These tests extract the real bash fences from the workflow files and
|
|
* execute them (with a stubbed `msd_run`) rather than string-matching the
|
|
* markdown — the same idiom tests/new-milestone-clear-phases.test.cjs uses.
|
|
*
|
|
* One file (not two) per scripts/lint-test-file-count.cjs's per-module cap —
|
|
* tests/workstream.test.cjs already owns the "workstream" module name, so
|
|
* this is the second and last slot; the two workflows are kept apart via
|
|
* top-level describe blocks instead of separate files.
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { runHook: runHookSeam } = require('./helpers/process-seam.cjs');
|
|
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
|
const { scanFencedBlocks } = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
/** Return the raw text of every ```bash fenced block in `text`. */
|
|
function extractBashBlocks(text) {
|
|
const lines = text.split(/\r?\n/);
|
|
const blocks = [];
|
|
for (const block of scanFencedBlocks(lines)) {
|
|
if (block.closeLineIdx === -1) continue;
|
|
if ((block.infoString || '').trim() !== 'bash') continue;
|
|
blocks.push(lines.slice(block.openLineIdx + 1, block.closeLineIdx).join('\n'));
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
// Locate the first ```bash fence strictly between two boundary strings.
|
|
function extractFenceBetween(markdown, startMarker, endMarker) {
|
|
const startIdx = markdown.indexOf(startMarker);
|
|
const endIdx = markdown.indexOf(endMarker);
|
|
assert.ok(startIdx !== -1, `marker not found: ${startMarker}`);
|
|
assert.ok(endIdx !== -1, `marker not found: ${endMarker}`);
|
|
assert.ok(startIdx < endIdx, `${startMarker} must precede ${endMarker}`);
|
|
const section = markdown.slice(startIdx, endIdx);
|
|
const bashBlocks = extractBashBlocks(section);
|
|
assert.ok(bashBlocks.length > 0, `no bash fence found between "${startMarker}" and "${endMarker}"`);
|
|
return bashBlocks[0];
|
|
}
|
|
|
|
// Locate the ```bash fence containing `marker`, between two boundary strings.
|
|
function extractFenceContaining(markdown, startMarker, endMarker, marker) {
|
|
const startIdx = markdown.indexOf(startMarker);
|
|
const endIdx = markdown.indexOf(endMarker);
|
|
assert.ok(startIdx !== -1 && endIdx !== -1 && startIdx < endIdx, 'boundary markers not found in order');
|
|
const section = markdown.slice(startIdx, endIdx);
|
|
for (const block of extractBashBlocks(section)) {
|
|
if (block.includes(marker)) return block;
|
|
}
|
|
assert.fail(`no bash fence containing "${marker}" found between "${startMarker}" and "${endMarker}"`);
|
|
return null;
|
|
}
|
|
|
|
describe('autonomous.md workstream-scoped paths (#4455)', () => {
|
|
const WORKFLOW_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'autonomous.md');
|
|
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
|
|
|
|
const discoverPhasesFence = extractFenceBetween(content, '## 2. Discover Phases', '## 3. Execute Phase');
|
|
const iterateFence = extractFenceBetween(content, '## 4. Iterate', '## 5. Lifecycle');
|
|
const lifecycle5bFence = extractFenceContaining(content, '## 5. Lifecycle', '## 6. Handle Blocker', 'ARCHIVE_DIR');
|
|
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-autonomous-ws-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
/**
|
|
* Write a canned init.manager-shaped JSON payload and a msd_run stub that
|
|
* returns it verbatim. Each call also appends one byte to a call-log file
|
|
* — NOT a shell variable increment, because `INIT_MANAGER=$(msd_run ...)`
|
|
* runs msd_run inside the command-substitution SUBSHELL, so a variable
|
|
* mutated there never survives back into the caller's shell.
|
|
*/
|
|
function stubMsdRun(jsonPayload) {
|
|
const jsonPath = path.join(tmpDir, 'init-manager.json');
|
|
const callLogPath = path.join(tmpDir, 'msd-run-calls.log');
|
|
fs.writeFileSync(jsonPath, JSON.stringify(jsonPayload));
|
|
fs.writeFileSync(callLogPath, '');
|
|
return `msd_run() { printf 'x' >> "${callLogPath}"; cat "${jsonPath}"; }\n`;
|
|
}
|
|
|
|
/** Number of msd_run invocations recorded by the most recent stubMsdRun-backed script. */
|
|
function msdRunCallCount() {
|
|
return fs.readFileSync(path.join(tmpDir, 'msd-run-calls.log'), 'utf8').length;
|
|
}
|
|
|
|
describe('discover_phases step: reads STATE.md from init.manager state_path, not a hardcoded literal', () => {
|
|
test('flat mode: no MSD_WORKSTREAM — resolves the root STATE.md path (regression guard)', () => {
|
|
const statePath = path.join(tmpDir, 'STATE-root.md');
|
|
fs.writeFileSync(statePath, '# Root State\n');
|
|
const script = `${stubMsdRun({ state_path: statePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <discover_phases fence>');
|
|
assert.ok(r.stdout.includes('STATE_CONTENT=[# Root State'),
|
|
`expected root STATE.md content, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: reads the workstream-scoped STATE.md, not the root one (#4455 regression)', () => {
|
|
const rootStatePath = path.join(tmpDir, 'STATE-root.md');
|
|
fs.writeFileSync(rootStatePath, '# Root State — must not be read\n');
|
|
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
|
|
fs.writeFileSync(wsStatePath, '# Workstream Alpha State\n');
|
|
|
|
const script = `${stubMsdRun({ state_path: wsStatePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <discover_phases fence>');
|
|
assert.ok(r.stdout.includes('STATE_CONTENT=[# Workstream Alpha State'),
|
|
`expected workstream STATE.md content, got: ${r.stdout}`);
|
|
assert.ok(!r.stdout.includes('Root State'),
|
|
`must not have read the root STATE.md, got: ${r.stdout}`);
|
|
});
|
|
});
|
|
|
|
describe('iterate step: single init.manager fetch backs both the re-filter and the fresh re-read', () => {
|
|
test('flat mode: resolves the root STATE.md path (regression guard)', () => {
|
|
const statePath = path.join(tmpDir, 'STATE-root.md');
|
|
fs.writeFileSync(statePath, '# Root State\n');
|
|
const script = `${stubMsdRun({ state_path: statePath })}${iterateFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <iterate fence>');
|
|
// The fence's own `cat "$STATE_PATH"` line prints the raw re-read to stdout.
|
|
assert.ok(r.stdout.includes('# Root State'), `expected root STATE.md content, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: re-reads the workstream STATE.md, not root (#4455 regression)', () => {
|
|
const rootStatePath = path.join(tmpDir, 'STATE-root.md');
|
|
fs.writeFileSync(rootStatePath, '# Root State — must not be read\n');
|
|
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
|
|
fs.writeFileSync(wsStatePath, '# Workstream Alpha State\n');
|
|
|
|
const script = `${stubMsdRun({ state_path: wsStatePath })}${iterateFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <iterate fence>');
|
|
assert.ok(r.stdout.includes('# Workstream Alpha State'),
|
|
`expected workstream STATE.md content, got: ${r.stdout}`);
|
|
assert.ok(!r.stdout.includes('Root State'),
|
|
`must not have read the root STATE.md, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('does not double-fetch init.manager within the iterate fence (no-double-fetch requirement)', () => {
|
|
const statePath = path.join(tmpDir, 'STATE-root.md');
|
|
fs.writeFileSync(statePath, '# Root State\n');
|
|
const script = `${stubMsdRun({ state_path: statePath })}${iterateFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <iterate fence>');
|
|
assert.strictEqual(msdRunCallCount(), 1,
|
|
`iterate fence must call msd_run exactly once (no double-fetch), got ${msdRunCallCount()}; stdout: ${r.stdout}`);
|
|
});
|
|
});
|
|
|
|
describe('lifecycle step 5b: archive existence check uses init.manager archive_dir, not a hardcoded literal', () => {
|
|
test('flat mode: checks the root milestones/ archive dir (regression guard)', () => {
|
|
const archiveDir = path.join(tmpDir, 'milestones-root');
|
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
fs.writeFileSync(path.join(archiveDir, 'v1.0-ROADMAP.md'), '# Archived Roadmap\n');
|
|
|
|
const script = `milestone_version="1.0"\n${stubMsdRun({ archive_dir: archiveDir })}${lifecycle5bFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <lifecycle 5b fence>');
|
|
// The fence concatenates with a literal bash `/` ("${ARCHIVE_DIR}/v...-ROADMAP.md"),
|
|
// never path.join — on Windows that yields a MIXED-separator path (backslashes
|
|
// from archiveDir + one trailing `/`), which path.join's all-backslash output
|
|
// does not match. Mirror the fence's own concatenation instead (#4455 CI finding).
|
|
assert.ok(r.stdout.includes(`${archiveDir}/v1.0-ROADMAP.md`),
|
|
`expected ls to find the root archive file, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: checks the workstream-scoped archive dir, not root (#4455 regression)', () => {
|
|
const rootArchiveDir = path.join(tmpDir, 'milestones-root');
|
|
fs.mkdirSync(rootArchiveDir, { recursive: true });
|
|
fs.writeFileSync(path.join(rootArchiveDir, 'v1.0-ROADMAP.md'), '# Root Archived Roadmap — must not be found\n');
|
|
|
|
const wsArchiveDir = path.join(tmpDir, 'milestones-alpha');
|
|
fs.mkdirSync(wsArchiveDir, { recursive: true });
|
|
fs.writeFileSync(path.join(wsArchiveDir, 'v1.0-ROADMAP.md'), '# Workstream Archived Roadmap\n');
|
|
|
|
const script = `milestone_version="1.0"\n${stubMsdRun({ archive_dir: wsArchiveDir })}${lifecycle5bFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <lifecycle 5b fence>');
|
|
// See the flat-mode test above for why this is a literal `/` join, not path.join.
|
|
assert.ok(r.stdout.includes(`${wsArchiveDir}/v1.0-ROADMAP.md`),
|
|
`expected ls to find the workstream archive file, got: ${r.stdout}`);
|
|
assert.ok(!r.stdout.includes(rootArchiveDir),
|
|
`must not have checked the root archive dir, got: ${r.stdout}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
|
|
const WORKFLOW_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'complete-milestone.md');
|
|
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
|
|
|
|
const STEP_START = '<step name="reorganize_roadmap_and_delete_originals">';
|
|
const STEP_END = '<step name="write_retrospective">';
|
|
|
|
const backlogFence = extractFenceContaining(content, STEP_START, STEP_END, 'BACKLOG_SECTION');
|
|
const sentinelFence = extractFenceContaining(content, STEP_START, STEP_END, '.msd-allow-shrink');
|
|
const commitFilesFence = extractFenceContaining(content, STEP_START, STEP_END, 'msd_run query commit');
|
|
const requirementsRmFence = extractFenceContaining(content, STEP_START, STEP_END, 'git rm');
|
|
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-complete-milestone-ws-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
/**
|
|
* Write a canned init.complete-milestone-shaped JSON payload and a msd_run
|
|
* stub that returns it for the `query init.complete-milestone` fetch, and
|
|
* echoes `msd_run_call:<argv>` for every OTHER call (e.g. `query commit
|
|
* ...`) — the same recording-stub idiom tests/new-milestone-clear-phases.
|
|
* test.cjs uses, so the commit `--files` list can be asserted on directly.
|
|
*/
|
|
function stubMsdRun(jsonPayload) {
|
|
const jsonPath = path.join(tmpDir, 'init-cm.json');
|
|
fs.writeFileSync(jsonPath, JSON.stringify(jsonPayload));
|
|
return `msd_run() { if [ "$1" = "query" ] && [ "$2" = "init.complete-milestone" ]; then cat "${jsonPath}"; else printf 'msd_run_call:%s\\n' "$*"; fi; }\n`;
|
|
}
|
|
|
|
describe('backlog extraction: reads ROADMAP.md from init.complete-milestone roadmap_path', () => {
|
|
test('flat mode: no MSD_WORKSTREAM — resolves the root ROADMAP.md path (regression guard)', () => {
|
|
const roadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
|
|
fs.writeFileSync(roadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Root backlog item\n');
|
|
const script = `${stubMsdRun({ roadmap_path: roadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <backlog fence>');
|
|
assert.ok(r.stdout.includes('Root backlog item'),
|
|
`expected root ROADMAP.md backlog content, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: reads the workstream-scoped ROADMAP.md, not root (#4455 regression)', () => {
|
|
const rootRoadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
|
|
fs.writeFileSync(rootRoadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Root backlog item — must not be read\n');
|
|
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
|
|
fs.writeFileSync(wsRoadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Workstream Alpha backlog item\n');
|
|
|
|
const script = `${stubMsdRun({ roadmap_path: wsRoadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <backlog fence>');
|
|
assert.ok(r.stdout.includes('Workstream Alpha backlog item'),
|
|
`expected workstream ROADMAP.md backlog content, got: ${r.stdout}`);
|
|
assert.ok(!r.stdout.includes('Root backlog item'),
|
|
`must not have read the root ROADMAP.md, got: ${r.stdout}`);
|
|
});
|
|
});
|
|
|
|
describe('write-guard sentinel: arms with init.complete-milestone roadmap_path', () => {
|
|
test('flat mode: sentinel names the root ROADMAP.md path (regression guard)', () => {
|
|
const roadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
|
|
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
|
|
const script = `${stubMsdRun({ roadmap_path: roadmapPath })}${sentinelFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <sentinel fence>');
|
|
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.msd-allow-shrink'), 'utf8').trim();
|
|
assert.strictEqual(sentinelContent, roadmapPath);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: sentinel names the workstream ROADMAP.md, not root (#4455 regression)', () => {
|
|
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
|
|
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
|
|
const script = `${stubMsdRun({ roadmap_path: wsRoadmapPath })}${sentinelFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <sentinel fence>');
|
|
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.msd-allow-shrink'), 'utf8').trim();
|
|
assert.strictEqual(sentinelContent, wsRoadmapPath);
|
|
assert.notStrictEqual(sentinelContent, path.join(tmpDir, '.planning', 'ROADMAP.md'));
|
|
});
|
|
});
|
|
|
|
describe('safety commit --files list: STATE/ROADMAP/archive/MILESTONES/PROJECT paths all scoped together', () => {
|
|
function runCommitFence(cmJson) {
|
|
const script = `${stubMsdRun(cmJson)}${commitFilesFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <commit --files fence>');
|
|
return r.stdout;
|
|
}
|
|
|
|
test('flat mode: --files lists root STATE/ROADMAP/archive/MILESTONES/PROJECT paths (regression guard)', () => {
|
|
const statePath = path.join(tmpDir, 'STATE-root.md');
|
|
const roadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
|
|
const archiveDir = path.join(tmpDir, 'milestones-root');
|
|
const milestonesPath = path.join(tmpDir, 'MILESTONES-root.md');
|
|
const projectPath = path.join(tmpDir, 'PROJECT-root.md');
|
|
const out = runCommitFence({
|
|
state_path: statePath, roadmap_path: roadmapPath, archive_dir: archiveDir,
|
|
milestones_path: milestonesPath, project_path: projectPath,
|
|
});
|
|
|
|
assert.ok(out.includes('msd_run_call:query commit'), `expected the commit call to be recorded, got: ${out}`);
|
|
assert.ok(out.includes(statePath), `expected root STATE.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(roadmapPath), `expected root ROADMAP.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(`${archiveDir}/v[X.Y]-ROADMAP.md`), `expected root archive ROADMAP in --files, got: ${out}`);
|
|
assert.ok(out.includes(milestonesPath), `expected root MILESTONES.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(projectPath), `expected root PROJECT.md in --files, got: ${out}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: --files lists workstream-scoped STATE/ROADMAP/archive/MILESTONES paths, but PROJECT.md STAYS root (#4455 regression)', () => {
|
|
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
|
|
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
|
|
const wsArchiveDir = path.join(tmpDir, 'milestones-alpha');
|
|
const wsMilestonesPath = path.join(tmpDir, 'MILESTONES-alpha.md');
|
|
// PROJECT.md is genuinely SHARED across workstreams — never cloned per
|
|
// workstream (msd-core/references/workstream-flag.md marks it
|
|
// `# Shared`; new-milestone.md states it outright; cmdWorkstreamCreate
|
|
// never creates one under a workstream dir). The real
|
|
// cmdInitCompleteMilestone therefore ALWAYS returns the root path for
|
|
// project_path regardless of MSD_WORKSTREAM — reflected here by
|
|
// passing the SAME root-shaped value the flat-mode test above uses,
|
|
// not a workstream-shaped one. An earlier version of this fix wrongly
|
|
// pinned MILESTONES.md and PROJECT.md as identically workstream-scoped
|
|
// (caught by isolated code review, then re-caught as a genuine
|
|
// regression after merge — see tests/init-manager.test.cjs's
|
|
// "milestones_path/project_path/requirements_path" describe block for
|
|
// the test that exercises the real init.cts function, not just this
|
|
// fence-mechanics stub).
|
|
const rootProjectPath = path.join(tmpDir, 'PROJECT-root.md');
|
|
const out = runCommitFence({
|
|
state_path: wsStatePath, roadmap_path: wsRoadmapPath, archive_dir: wsArchiveDir,
|
|
milestones_path: wsMilestonesPath, project_path: rootProjectPath,
|
|
});
|
|
|
|
assert.ok(out.includes(wsStatePath), `expected workstream STATE.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(wsRoadmapPath), `expected workstream ROADMAP.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(`${wsArchiveDir}/v[X.Y]-ROADMAP.md`), `expected workstream archive ROADMAP in --files, got: ${out}`);
|
|
assert.ok(out.includes(wsMilestonesPath), `expected workstream MILESTONES.md in --files, got: ${out}`);
|
|
assert.ok(out.includes(rootProjectPath), `expected root PROJECT.md in --files, got: ${out}`);
|
|
assert.ok(!out.includes(path.join(tmpDir, '.planning', 'STATE.md')),
|
|
`must not fall back to the flat root STATE.md path, got: ${out}`);
|
|
assert.ok(!out.includes(path.join(tmpDir, '.planning', 'ROADMAP.md')),
|
|
`must not fall back to the flat root ROADMAP.md path, got: ${out}`);
|
|
assert.ok(!out.includes(path.join(tmpDir, '.planning', 'MILESTONES.md')),
|
|
`must not fall back to the flat root MILESTONES.md path, got: ${out}`);
|
|
});
|
|
});
|
|
|
|
describe('REQUIREMENTS.md removal: git rm uses init.complete-milestone requirements_path, not a hardcoded literal', () => {
|
|
test('flat mode: removes the root REQUIREMENTS.md path (regression guard)', () => {
|
|
const requirementsPath = path.join(tmpDir, 'REQUIREMENTS-root.md');
|
|
fs.writeFileSync(requirementsPath, '# Requirements\n');
|
|
fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true }); // git rm needs a repo; the stub below intercepts it
|
|
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubMsdRun({ requirements_path: requirementsPath })}${requirementsRmFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <requirements rm fence>');
|
|
assert.ok(r.stdout.includes(`git_call:rm ${requirementsPath}`),
|
|
`expected git rm to target the root REQUIREMENTS.md, got: ${r.stdout}`);
|
|
});
|
|
|
|
test('MSD_WORKSTREAM=alpha: removes the workstream-scoped REQUIREMENTS.md, not root (#4455 follow-up regression)', () => {
|
|
const wsRequirementsPath = path.join(tmpDir, 'REQUIREMENTS-alpha.md');
|
|
fs.writeFileSync(wsRequirementsPath, '# Requirements\n');
|
|
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubMsdRun({ requirements_path: wsRequirementsPath })}${requirementsRmFence}`;
|
|
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
|
|
throwIfFailed(r, 'bash <requirements rm fence>');
|
|
assert.ok(r.stdout.includes(`git_call:rm ${wsRequirementsPath}`),
|
|
`expected git rm to target the workstream REQUIREMENTS.md, got: ${r.stdout}`);
|
|
assert.ok(!r.stdout.includes('git_call:rm .planning/REQUIREMENTS.md'),
|
|
`must not have targeted the literal root REQUIREMENTS.md path, got: ${r.stdout}`);
|
|
});
|
|
});
|
|
});
|