Files
msd-core/tests/bug-3042-3044-research-flag-and-stale-refs.test.cjs

356 lines
15 KiB
JavaScript

// allow-test-rule: prose-driven workflow files. The .includes() / regex
// hits below build a typed record of (a) presence of slash-command tokens
// in the parsed argument-hint frontmatter, (b) the literal flag tokens
// the workflow's bash parsing block consults, and (c) absence of deleted
// slash-command tokens across user-facing surfaces. These workflow files
// ARE the implementation — there is no "real" code seam to assert against.
// The IR-build hits are not the assertion surface; assertions run on the
// resulting boolean / Set membership.
/**
* Bug #3042 + #3044: research-only flag for /gsd-plan-phase + scrub of
* 4 stale slash-command references across user-facing surfaces.
*
* #3042 (orphaned research-phase): the slash command /gsd-research-phase
* never had a stub registered. Per the maintainer decision, the
* capability moves to a flag on /gsd-plan-phase rather than restoring
* a separate command. Invocation:
*
* /gsd-plan-phase --research-phase <N>
*
* When --research-phase is present, plan-phase scopes to phase N,
* runs only the research step, and exits before spawning the planner /
* plan-checker / verifier chain.
*
* #3044 (stale slash-command refs in user-facing docs): four commands
* appear in workflow / template / doc surfaces without being
* registered:
*
* /gsd-check-todos → /gsd-capture --list
* /gsd-new-workspace → /gsd-workspace --new
* /gsd-plan-milestone-gaps → inline gap planning (#3038 partial scrub)
* /gsd-status → /gsd-progress
* /gsd-research-phase → /gsd-plan-phase --research-phase
*
* Tests assert (a) the flag is wired, (b) every deleted/never-registered
* slash-command token is absent from user-facing surfaces, (c) the
* orphaned workflows/research-phase.md is removed.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
function read(rel) {
return fs.readFileSync(path.join(ROOT, rel), 'utf-8');
}
function exists(rel) {
return fs.existsSync(path.join(ROOT, rel));
}
// ─── Slash-command token extractor (typed-IR helper) ────────────────────────
/**
* Returns the Set<string> of `/gsd-*` slash-command tokens emitted by a
* markdown surface. Strips trailing arg tokens so the identity is the
* command name only.
*/
function extractSlashCommandTokens(content) {
const re = /\/gsd-[a-z0-9][a-z0-9-]*/g;
return new Set(content.match(re) || []);
}
// ─── #3042: --research-phase flag wired into /gsd-plan-phase ────────────────
describe('bug #3042: /gsd-plan-phase --research-phase flag absorbs the standalone research command', () => {
test('commands/gsd/plan-phase.md argument-hint advertises --research-phase', () => {
const content = read('commands/gsd/plan-phase.md');
// Frontmatter argument-hint is the structural place users discover
// the flag. Parse the line that starts with "argument-hint:" and
// assert the flag token is present.
const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
assert.ok(
m[1].includes('--research-phase'),
`argument-hint must include "--research-phase"; got: ${m[1]}`
);
});
test('plan-phase.md frontmatter description still advertises plan capability (no semantics drift)', () => {
const content = read('commands/gsd/plan-phase.md');
const m = content.match(/^description:\s*(.+)$/m);
assert.ok(m, 'plan-phase.md must have a description field');
// The description should still describe planning — the flag is
// additive, not a renamed command.
assert.ok(
/plan/i.test(m[1]),
`description should still mention planning; got: ${m[1]}`
);
});
test('workflows/plan-phase.md parses --research-phase and sets a research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The arg-parsing section of the workflow must mention the new flag
// by name. This is the structural seam the LLM follows.
assert.ok(
content.includes('--research-phase'),
'plan-phase.md workflow must reference the --research-phase flag in its argument-parsing section'
);
});
test('workflows/plan-phase.md skips planner/verifier when in research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// Look for explicit early-exit prose so the LLM knows to stop after
// research. We accept any of: "research-only", "research only mode",
// "skip if --research-phase", "RESEARCH_ONLY", "exit after research".
const patterns = [
/research[ -]only/i,
/RESEARCH_ONLY/,
/skip if[^\n]*--research-phase/i,
/exit (?:after|when)[^\n]*research/i,
];
const hits = patterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must contain explicit early-exit prose for --research-phase mode; ` +
`none of [research-only, RESEARCH_ONLY, "skip if --research-phase", "exit after research"] matched`
);
});
test('orphaned workflows/research-phase.md is removed', () => {
assert.equal(
exists('get-shit-done/workflows/research-phase.md'),
false,
'workflows/research-phase.md must be removed; the capability now lives on /gsd-plan-phase --research-phase'
);
});
test('argument-hint advertises --view as a research-only modifier', () => {
const content = read('commands/gsd/plan-phase.md');
const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
assert.ok(
m[1].includes('--view'),
`argument-hint must include --view (research-only view-only mode); got: ${m[1]}`
);
});
test('workflow handles --view by printing existing RESEARCH.md without spawning', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The workflow must reference the --view flag as a no-spawn mode
// for research-only invocations. We accept any of: "view-only",
// "VIEW_ONLY", "skip if --view", "no spawn" alongside --view.
assert.ok(
/--view/.test(content),
'plan-phase workflow must reference the --view flag'
);
const viewModePatterns = [
/view[ -]only/i,
/VIEW_ONLY/,
/no[ -]spawn/i,
/print[^\n]*RESEARCH\.md/i,
/display[^\n]*RESEARCH\.md/i,
];
const hits = viewModePatterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must explain that --view prints existing RESEARCH.md without spawning; ` +
`expected one of [view-only, VIEW_ONLY, no-spawn, "print/display RESEARCH.md"]`
);
});
test('workflow uses --research as the force-refresh signal in research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The plan-phase workflow already had a --research flag with
// "force re-research" semantics. In research-only mode, that flag
// must short-circuit the "RESEARCH.md exists, what do you want to
// do?" prompt and unconditionally re-spawn. Assert the workflow
// documents the combined semantics.
const forceRefreshPatterns = [
/--research[^\n]*force[^\n]*refresh/i,
/--research[^\n]*re[ -]?research/i,
/force[ -]?refresh[^\n]*--research/i,
];
const hits = forceRefreshPatterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must document that --research forces re-research (skip the "exists" prompt) when used with --research-phase`
);
});
test('workflow has an existing-RESEARCH.md prompt path (update/view/skip) within proximity', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// CR #3045 finding: the previous version of this test asserted
// `update`, `view`, `skip` appeared anywhere in the file, which was
// tautological — those words occur all over the workflow for
// unrelated reasons (--skip-research, --view flag declarations,
// etc.). Tighten to a proximity check: all three choice tokens
// must occur in a window of ~400 chars surrounding "RESEARCH.md
// already exists" / "Update — re-spawn" / equivalent prompt prose,
// proving the prompt section is genuinely present.
const idx = content.indexOf('RESEARCH.md already exists');
assert.ok(
idx >= 0,
'plan-phase workflow must contain the literal "RESEARCH.md already exists" prompt header in the research-only existing-artifact section'
);
const window = content.slice(idx, idx + 600);
const hasUpdate = /\b(?:update|refresh|re-spawn)\b/i.test(window);
const hasView = /\bview\b/i.test(window);
const hasSkip = /\bskip\b/i.test(window);
assert.ok(
hasUpdate && hasView && hasSkip,
`prompt section near "RESEARCH.md already exists" must mention all three choices (update/refresh/re-spawn, view, skip); ` +
`got update=${hasUpdate} view=${hasView} skip=${hasSkip}`
);
});
});
// ─── #3044: every deleted slash-command absent from user-facing surfaces ────
const DELETED_COMMANDS = [
'/gsd-check-todos', // → /gsd-capture --list
'/gsd-new-workspace', // → /gsd-workspace --new
'/gsd-status', // → /gsd-progress
'/gsd-plan-milestone-gaps', // → inline gap planning
'/gsd-research-phase', // → /gsd-plan-phase --research-phase
];
// Surfaces a user reads, browses, or follows routing prose from. Each
// must not emit any of the deleted slash-command tokens. Localized doc
// sets are included so the rename actually reaches every reader.
const USER_FACING_SURFACES = [
// Top-level repo
'README.md',
// Primary docs
'docs/USER-GUIDE.md',
'docs/FEATURES.md',
'docs/INVENTORY.md',
'docs/COMMANDS.md',
'docs/issue-driven-orchestration.md',
// Workflow surfaces that emit user-typed slash commands
'get-shit-done/workflows/check-todos.md',
'get-shit-done/workflows/add-todo.md',
'get-shit-done/workflows/resume-project.md',
'get-shit-done/workflows/progress.md',
'get-shit-done/workflows/code-review.md',
'get-shit-done/workflows/new-workspace.md',
'get-shit-done/workflows/list-workspaces.md',
'get-shit-done/workflows/transition.md',
'get-shit-done/workflows/discovery-phase.md',
// Reference + template surfaces
'get-shit-done/references/continuation-format.md',
'get-shit-done/templates/state.md',
'get-shit-done/templates/discovery.md',
'get-shit-done/templates/README.md',
];
describe('bug #3044: deleted slash-commands scrubbed from user-facing surfaces', () => {
for (const rel of USER_FACING_SURFACES) {
test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
if (!exists(rel)) {
// Some surfaces may not exist in every repo state (e.g. when the
// workflow file is itself removed in this PR). Skip with a
// structural note rather than a hard failure — the deletion is
// verified by extractSlashCommandTokens against the surfaces
// that DO exist.
return;
}
const tokens = extractSlashCommandTokens(read(rel));
for (const cmd of DELETED_COMMANDS) {
assert.equal(
tokens.has(cmd),
false,
`${rel}: parsed slash-command token set still contains deleted "${cmd}"`
);
}
});
}
});
// ─── Localized doc sets must also be scrubbed ───────────────────────────────
const LOCALES = ['ja-JP', 'ko-KR', 'zh-CN', 'pt-BR'];
const LOCALIZED_DOCS = [
'README.md',
'USER-GUIDE.md',
'FEATURES.md',
'COMMANDS.md',
];
describe('bug #3044: localized doc sets also scrubbed', () => {
for (const locale of LOCALES) {
for (const doc of LOCALIZED_DOCS) {
const rel = path.posix.join('docs', locale, doc);
test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
if (!exists(rel)) return; // some locales may not have every doc
const tokens = extractSlashCommandTokens(read(rel));
for (const cmd of DELETED_COMMANDS) {
assert.equal(
tokens.has(cmd),
false,
`${rel}: parsed slash-command token set still contains deleted "${cmd}"`
);
}
});
}
}
});
// ─── Replacement commands are documented ────────────────────────────────────
describe('bug #3094: progress routing does not reference removed /gsd-list-phase-assumptions', () => {
test('get-shit-done/workflows/progress.md has no /gsd-list-phase-assumptions token', () => {
const content = read('get-shit-done/workflows/progress.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-list-phase-assumptions'),
false,
'progress.md must not recommend removed /gsd-list-phase-assumptions'
);
});
test('progress.md pre-planning guidance uses /gsd-discuss-phase instead', () => {
const content = read('get-shit-done/workflows/progress.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-discuss-phase'),
true,
'progress.md should route pre-planning assumption checks via /gsd-discuss-phase'
);
});
});
describe('replacement commands appear where the deleted ones used to live', () => {
test('docs/issue-driven-orchestration.md uses /gsd-workspace --new (not /gsd-new-workspace)', () => {
const content = read('docs/issue-driven-orchestration.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-workspace'),
true,
'issue-driven-orchestration.md must reference /gsd-workspace as the workspace command'
);
// The "--new" flag must appear within 60 chars of /gsd-workspace —
// proves the flag belongs to that command rather than appearing
// somewhere unrelated.
const proximityRe = /\/gsd-workspace[^\n]{0,60}--new/;
assert.ok(
proximityRe.test(content),
'issue-driven-orchestration.md must document the "/gsd-workspace … --new" form'
);
});
test('workflows/code-review.md error path points at /gsd-progress (not /gsd-status)', () => {
const content = read('get-shit-done/workflows/code-review.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-progress'),
true,
'code-review.md error path must route the user at /gsd-progress'
);
});
});