refactor: hard-fork GSD -> MSD (Make Software Done)
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.
This commit is contained in:
@@ -3,10 +3,10 @@
|
||||
/**
|
||||
* Unit tests for the smart-entry situation classifier.
|
||||
*
|
||||
* Spec: docs/superpowers/specs/2026-06-27-gsd-smart-entry-design.md
|
||||
* Spec: docs/superpowers/specs/2026-06-27-msd-smart-entry-design.md
|
||||
*
|
||||
* Covers: all 11 situations, priority ordering (paused beats blocked), JSON
|
||||
* shape invariants (exactly one recommended, commands are /gsd:* slash forms),
|
||||
* shape invariants (exactly one recommended, commands are /msd:* slash forms),
|
||||
* and the --json / human output modes.
|
||||
*/
|
||||
|
||||
@@ -20,9 +20,9 @@ const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { gitOrThrow, throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const smartEntry = require('../gsd-core/bin/lib/smart-entry.cjs');
|
||||
const smartEntry = require('../msd-core/bin/lib/smart-entry.cjs');
|
||||
const { classify, classifyProject, detectSignals, SITUATIONS } = smartEntry;
|
||||
const TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const TOOLS = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
|
||||
|
||||
// ─── Fixture helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -31,7 +31,7 @@ const TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
* @returns {string} tmpDir path
|
||||
*/
|
||||
function makeProject({ state, roadmap = false, git = false, verifyFail = false } = {}) {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-'));
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-'));
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
|
||||
if (state !== undefined) {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), state);
|
||||
@@ -92,7 +92,7 @@ describe('smart-entry: situation coverage', () => {
|
||||
const CASES = [
|
||||
['no-project', () => {
|
||||
// Truly empty dir — no .planning at all.
|
||||
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-none-'));
|
||||
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-none-'));
|
||||
return track(d);
|
||||
}],
|
||||
['paused', () => track(makeProject({ state: state({ status: 'planning', paused_at: '2026-06-01T00:00:00Z' }), roadmap: true }))],
|
||||
@@ -204,7 +204,7 @@ describe('smart-entry: idle-stranded (git-dependent)', () => {
|
||||
const actions = smartEntry.actionsFor(situation, forced);
|
||||
assert.equal(actions[0].id, 'ship');
|
||||
assert.equal(actions[0].recommended, true);
|
||||
assert.equal(actions[0].command, '/gsd:ship');
|
||||
assert.equal(actions[0].command, '/msd:ship');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('smart-entry: priority ordering', () => {
|
||||
});
|
||||
|
||||
test('no-project beats everything (no .planning)', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-empty-'));
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-empty-'));
|
||||
track(tmpDir);
|
||||
const result = classifyProject(tmpDir);
|
||||
assert.equal(result.situation, 'no-project');
|
||||
@@ -303,7 +303,7 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
|
||||
function realState({ status, phase, totalPhases, percent }) {
|
||||
const fm = [
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'msd_state_version: 1.0',
|
||||
`status: ${status}`,
|
||||
'last_activity: 2026-06-13',
|
||||
'progress:',
|
||||
@@ -352,7 +352,7 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
|
||||
}));
|
||||
const result = classifyProject(dir);
|
||||
assert.equal(result.situation, 'executing');
|
||||
// Forward motion delegates to /gsd:progress --next, not a raw execute-phase.
|
||||
// Forward motion delegates to /msd:progress --next, not a raw execute-phase.
|
||||
assert.equal(result.recommended, 'progress-next');
|
||||
});
|
||||
});
|
||||
@@ -360,14 +360,14 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
|
||||
describe('smart-entry: in-project advancement delegates to the gated engine', () => {
|
||||
afterEach(removeAll);
|
||||
|
||||
// Reconciliation guard (#1787): /gsd:next must not re-implement forward routing.
|
||||
// Reconciliation guard (#1787): /msd:next must not re-implement forward routing.
|
||||
// For every in-project forward-motion situation the recommended action is
|
||||
// `/gsd:progress --next` (workflows/next.md), so Route 0's resume-incomplete
|
||||
// `/msd:progress --next` (workflows/next.md), so Route 0's resume-incomplete
|
||||
// -phase invariant + Gates 1-3 are never bypassed. Re-deriving advancement here
|
||||
// is what got the old flat /gsd-next removed (#3054). The specific command
|
||||
// is what got the old flat /msd-next removed (#3054). The specific command
|
||||
// (execute-phase / plan-phase / verify-work) stays as an explicit secondary.
|
||||
for (const situation of ['planning', 'executing', 'verify-pending']) {
|
||||
test(`${situation}: recommended action is /gsd:progress --next`, () => {
|
||||
test(`${situation}: recommended action is /msd:progress --next`, () => {
|
||||
const signals = {
|
||||
current_phase: 2, total_phases: 5, status: situation, progress: 40,
|
||||
has_planning: true, has_roadmap: true, git_dirty: false, git_unpushed: false,
|
||||
@@ -376,19 +376,19 @@ describe('smart-entry: in-project advancement delegates to the gated engine', ()
|
||||
const actions = smartEntry.actionsFor(situation, signals);
|
||||
const recommended = actions.filter((a) => a.recommended);
|
||||
assert.equal(recommended.length, 1, 'exactly one recommended action');
|
||||
assert.equal(recommended[0].command, '/gsd:progress --next');
|
||||
assert.equal(recommended[0].command, '/msd:progress --next');
|
||||
});
|
||||
}
|
||||
|
||||
// Remediation / lifecycle situations are OFF the linear advance path — they
|
||||
// keep direct specific recommendations (their distinct value over --next).
|
||||
const DIRECT = {
|
||||
'no-project': '/gsd:new-project',
|
||||
paused: '/gsd:resume-work',
|
||||
blocked: '/gsd:debug',
|
||||
'verify-failed': '/gsd:verify-work',
|
||||
'idle-stranded': '/gsd:ship',
|
||||
complete: '/gsd:new-milestone',
|
||||
'no-project': '/msd:new-project',
|
||||
paused: '/msd:resume-work',
|
||||
blocked: '/msd:debug',
|
||||
'verify-failed': '/msd:verify-work',
|
||||
'idle-stranded': '/msd:ship',
|
||||
complete: '/msd:new-milestone',
|
||||
};
|
||||
for (const [situation, command] of Object.entries(DIRECT)) {
|
||||
test(`${situation}: keeps its direct recommendation (${command})`, () => {
|
||||
@@ -407,7 +407,7 @@ describe('smart-entry: in-project advancement delegates to the gated engine', ()
|
||||
describe('smart-entry: per-situation action invariants (all 11)', () => {
|
||||
// Lock the action-set contract for EVERY situation, not just the 6 sampled by
|
||||
// the JSON-shape test: exactly one recommended, 1-4 actions, unique ids, and
|
||||
// /gsd:* command forms. Guards the reconciliation (and future edits) against
|
||||
// /msd:* command forms. Guards the reconciliation (and future edits) against
|
||||
// silently breaking these for a less-common situation.
|
||||
const sampleSignals = {
|
||||
current_phase: 2, total_phases: 5, status: 'executing', progress: 60,
|
||||
@@ -415,14 +415,14 @@ describe('smart-entry: per-situation action invariants (all 11)', () => {
|
||||
paused: false, blockers: [], has_git: true, verify_failed: false, stale_activity: false,
|
||||
};
|
||||
for (const situation of SITUATIONS) {
|
||||
test(`${situation}: exactly one recommended, 1-4 unique-id /gsd:* actions`, () => {
|
||||
test(`${situation}: exactly one recommended, 1-4 unique-id /msd:* actions`, () => {
|
||||
const actions = smartEntry.actionsFor(situation, sampleSignals);
|
||||
assert.ok(actions.length >= 1 && actions.length <= 4, `${situation}: 1-4 actions (got ${actions.length})`);
|
||||
assert.equal(actions.filter((a) => a.recommended).length, 1, `${situation}: exactly one recommended`);
|
||||
const ids = actions.map((a) => a.id);
|
||||
assert.equal(new Set(ids).size, ids.length, `${situation}: action ids are unique`);
|
||||
for (const a of actions) {
|
||||
assert.ok(a.command.startsWith('/gsd:'), `${situation}/${a.id}: command is a /gsd: slash form`);
|
||||
assert.ok(a.command.startsWith('/msd:'), `${situation}/${a.id}: command is a /msd: slash form`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -431,7 +431,7 @@ describe('smart-entry: per-situation action invariants (all 11)', () => {
|
||||
describe('smart-entry: JSON shape invariants', () => {
|
||||
afterEach(removeAll);
|
||||
|
||||
test('every situation yields exactly one recommended action and /gsd:* commands', () => {
|
||||
test('every situation yields exactly one recommended action and /msd:* commands', () => {
|
||||
const dirs = [
|
||||
track(makeProject()), // no-project
|
||||
track(makeProject({ state: state({ status: 'planning', paused_at: '2026-06-01T00:00:00Z' }), roadmap: true })), // paused
|
||||
@@ -446,7 +446,7 @@ describe('smart-entry: JSON shape invariants', () => {
|
||||
assert.equal(recommended.length, 1, `${result.situation}: exactly one recommended`);
|
||||
assert.equal(recommended[0].id, result.recommended, `${result.situation}: recommended id matches`);
|
||||
for (const a of result.actions) {
|
||||
assert.ok(a.command.startsWith('/gsd:'), `${result.situation}/${a.id}: command is a slash form`);
|
||||
assert.ok(a.command.startsWith('/msd:'), `${result.situation}/${a.id}: command is a slash form`);
|
||||
}
|
||||
assert.ok(result.summary.length > 0, `${result.situation}: summary non-empty`);
|
||||
assert.ok(result.actions.length >= 1 && result.actions.length <= 4, `${result.situation}: 1-4 actions`);
|
||||
@@ -454,7 +454,7 @@ describe('smart-entry: JSON shape invariants', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
|
||||
describe('smart-entry: CLI dispatch (msd-tools smart-entry)', () => {
|
||||
afterEach(removeAll);
|
||||
|
||||
test('#4023: --json preserves decimal current_phase as a phase-id string', () => {
|
||||
@@ -464,7 +464,7 @@ describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
|
||||
roadmap: true,
|
||||
}));
|
||||
const r = runNode([TOOLS, 'smart-entry', '--json', '--cwd', dir], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(r, 'gsd-tools smart-entry --json');
|
||||
throwIfFailed(r, 'msd-tools smart-entry --json');
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.signals.current_phase, phase);
|
||||
}
|
||||
@@ -472,21 +472,21 @@ describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
|
||||
|
||||
test('--json in an empty dir returns no-project machine JSON', () => {
|
||||
// A bare tmpdir with no .planning is a true no-project.
|
||||
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-bare-'));
|
||||
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-bare-'));
|
||||
track(bare);
|
||||
const r = runNode([TOOLS, 'smart-entry', '--json', '--cwd', bare], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(r, 'gsd-tools smart-entry --json');
|
||||
throwIfFailed(r, 'msd-tools smart-entry --json');
|
||||
const j = JSON.parse(r.stdout);
|
||||
assert.equal(j.situation, 'no-project');
|
||||
assert.equal(j.recommended, 'new-project');
|
||||
assert.equal(j.actions[0].command, '/gsd:new-project');
|
||||
assert.equal(j.actions[0].command, '/msd:new-project');
|
||||
});
|
||||
|
||||
test('default (human) mode prints a plain summary line, not JSON', () => {
|
||||
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-human-'));
|
||||
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-human-'));
|
||||
track(bare);
|
||||
const r = runNode([TOOLS, 'smart-entry', '--cwd', bare], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(r, 'gsd-tools smart-entry');
|
||||
throwIfFailed(r, 'msd-tools smart-entry');
|
||||
const out = r.stdout;
|
||||
assert.ok(!out.startsWith('{'), 'human mode is not JSON');
|
||||
assert.match(out, /No project yet/);
|
||||
@@ -618,7 +618,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
|
||||
// not depend on the wall clock.
|
||||
const FIXED_NOW = () => Date.parse('2026-08-01T00:00:00Z');
|
||||
|
||||
// gsd-core's own STATE.md carries last_activity as "YYYY-MM-DD — <description>"
|
||||
// msd-core's own STATE.md carries last_activity as "YYYY-MM-DD — <description>"
|
||||
// (templates/state.md prescribes `Last activity: [YYYY-MM-DD] — [What happened]`
|
||||
// for the body; the frontmatter mirrors it). Before the fix, parseActivityTimestamp
|
||||
// ran Date.parse on the whole string → NaN → staleActivity failed OPEN to false,
|
||||
@@ -628,7 +628,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
|
||||
test('frontmatter last_activity with " — description" suffix is detected stale', () => {
|
||||
const stateMd = [
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'msd_state_version: 1.0',
|
||||
'status: executing',
|
||||
'last_activity: 2026-06-08 — Milestone 2 executed autonomously (all passed)',
|
||||
'progress:',
|
||||
@@ -975,7 +975,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
|
||||
const {
|
||||
_resetUnusableInputWarningsForTests,
|
||||
_unusableInputEmissionCountForTests,
|
||||
} = require('../gsd-core/bin/lib/unusable-input.cjs');
|
||||
} = require('../msd-core/bin/lib/unusable-input.cjs');
|
||||
_resetUnusableInputWarningsForTests();
|
||||
const stateMd = [
|
||||
'---',
|
||||
@@ -1007,7 +1007,7 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
|
||||
afterEach(() => { while (dirs.length) cleanup(dirs.pop()); });
|
||||
|
||||
function gitProject(stateHead) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-se-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-se-'));
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
runGit(['init', '-q'], { cwd: dir });
|
||||
runGit(['config', 'user.email', 't@t.com'], { cwd: dir });
|
||||
@@ -1080,7 +1080,7 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
|
||||
});
|
||||
|
||||
test('non-git project → null (no signal), never throws', () => {
|
||||
const dir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-nogit-')));
|
||||
const dir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-nogit-')));
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.planning', 'STATE.md'),
|
||||
@@ -1101,9 +1101,9 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
|
||||
const {
|
||||
_resetUnusableInputWarningsForTests,
|
||||
_unusableInputEmissionCountForTests,
|
||||
} = require('../gsd-core/bin/lib/unusable-input.cjs');
|
||||
} = require('../msd-core/bin/lib/unusable-input.cjs');
|
||||
_resetUnusableInputWarningsForTests();
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-compose-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-compose-'));
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
runGit(['init', '-q'], { cwd: dir });
|
||||
runGit(['config', 'user.email', 't@t.com'], { cwd: dir });
|
||||
@@ -1149,7 +1149,7 @@ describe('#3099: unusable last_activity emits a diagnostic', () => {
|
||||
const {
|
||||
_resetUnusableInputWarningsForTests,
|
||||
_unusableInputEmissionCountForTests,
|
||||
} = require('../gsd-core/bin/lib/unusable-input.cjs');
|
||||
} = require('../msd-core/bin/lib/unusable-input.cjs');
|
||||
|
||||
function makeStateWithActivity(activity) {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user