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.
204 lines
8.4 KiB
JavaScript
204 lines
8.4 KiB
JavaScript
'use strict';
|
|
/**
|
|
* state-command-cutover.test.cjs — ADR-2346 (epic #2345) P1 equivalence tests.
|
|
*
|
|
* Verifies that `state`, after cutover from the hardcoded `case 'state':` arm
|
|
* in msd-tools.cjs to the host dispatch table (dispatchHostCommand, consulted
|
|
* in runCommand's `default` case), behaves identically to the old inline case.
|
|
*
|
|
* Dispatch path after cutover:
|
|
* runCommand default → dispatchHostCommand → HOST_COMMAND_ROUTERS.state
|
|
* → routeStateCommand({ state, args, cwd, raw, error })
|
|
*
|
|
* Test categories (mirrors tests/audit-command-cutover.test.cjs):
|
|
* 1. UNIT — dispatchHostCommand return values + prototype-pollution guard
|
|
* 2. DISPATCH — `state <sub>` reaches the router via the host table (end-to-end)
|
|
* 3. BEHAVIOR — real output-shape assertions for `state load` + unknown subcommand
|
|
* 4. JSON-ERRORS — unknown subcommand produces the canonical error
|
|
* 5. REGISTRY — HOST_COMMAND_ROUTERS owns `state`
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const { runMsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
// msd-tools.cjs is hand-authored (committed, not generated) — safe to require.
|
|
const {
|
|
dispatchHostCommand,
|
|
HOST_COMMAND_ROUTERS,
|
|
} = require('../msd-core/bin/msd-tools.cjs');
|
|
|
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
function makeErrorRecorder() {
|
|
const calls = [];
|
|
const fn = (msg, reason) => calls.push({ msg, reason });
|
|
fn.calls = calls;
|
|
return fn;
|
|
}
|
|
|
|
// ─── 1. UNIT — dispatchHostCommand return values + pollution guard ───────────
|
|
|
|
describe('dispatchHostCommand: unit', async () => {
|
|
const CWD = '/fake/cwd';
|
|
const RAW = false;
|
|
|
|
test('returns true for the migrated `state` command (consumed)', async () => {
|
|
// routeStateCommand will run against a fake cwd; it may call error() for a
|
|
// missing subcommand — that is fine, we only assert the dispatch returned
|
|
// true (consumed) rather than falling through to "Unknown command".
|
|
const errFn = makeErrorRecorder();
|
|
const consumed = await dispatchHostCommand({
|
|
command: 'state',
|
|
args: ['state'],
|
|
cwd: CWD,
|
|
raw: RAW,
|
|
error: errFn,
|
|
});
|
|
assert.strictEqual(consumed, true, 'state must be consumed by the host table');
|
|
});
|
|
|
|
test('all migrated Tier-1 host commands are consumed by the host table', async () => {
|
|
// Each migrated router receives its module-scope lib via the table entry;
|
|
// against a fake cwd it may emit an error (missing subcommand), but the
|
|
// dispatch itself must report consumed=true (no fall-through to the
|
|
// unknown-command error).
|
|
// NOTE: `capability` (P2) and the P3 routers are omitted from this INVOCATION
|
|
// loop — they delegate to module functions (commands.cmd*, config.cmd*,
|
|
// _dispatchNonFamily, require()) that call the MODULE-SCOPE error() (which
|
|
// exits the process) rather than the injected mock. They are covered by the
|
|
// registry-ownership assertion below (non-invoking) and by their own
|
|
// dedicated behavioral test suites (resolve/git/config/research tests).
|
|
for (const cmd of ['state', 'phase', 'init', 'roadmap', 'validate', 'verify']) {
|
|
const errFn = makeErrorRecorder();
|
|
const consumed = await dispatchHostCommand({
|
|
command: cmd,
|
|
args: [cmd],
|
|
cwd: CWD,
|
|
raw: RAW,
|
|
error: errFn,
|
|
});
|
|
assert.strictEqual(consumed, true, `${cmd} must be consumed by the host table`);
|
|
}
|
|
});
|
|
|
|
test('returns false for an unknown command (fall through)', async () => {
|
|
const errFn = makeErrorRecorder();
|
|
const consumed = await dispatchHostCommand({
|
|
command: 'not-a-real-command',
|
|
args: ['not-a-real-command'],
|
|
cwd: CWD,
|
|
raw: RAW,
|
|
error: errFn,
|
|
});
|
|
assert.strictEqual(consumed, false, 'unknown command must fall through');
|
|
assert.strictEqual(errFn.calls.length, 0, 'error must not be called for a miss');
|
|
});
|
|
|
|
test('prototype-pollution guard: __proto__/constructor/prototype fall through', async () => {
|
|
for (const bad of ['__proto__', 'constructor', 'prototype']) {
|
|
const errFn = makeErrorRecorder();
|
|
const consumed = await dispatchHostCommand({
|
|
command: bad,
|
|
args: [bad],
|
|
cwd: CWD,
|
|
raw: RAW,
|
|
error: errFn,
|
|
});
|
|
assert.strictEqual(consumed, false, `${bad} must not be dispatched`);
|
|
assert.strictEqual(errFn.calls.length, 0, `${bad} must not call error`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── 2 + 3. DISPATCH + BEHAVIOR — end-to-end via runMsdTools ─────────────────
|
|
|
|
describe('state cutover: end-to-end dispatch via the host table', async () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('`state load` succeeds end-to-end (dispatched via host table, not a case arm)', async () => {
|
|
// Seed a minimal STATE.md so `state load` has something to read.
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
'# Project State\n\n## Current Position\n\nPhase: 1 of 1 (Test)\n',
|
|
);
|
|
|
|
const result = runMsdTools(['--cwd=' + tmpDir, 'state', 'load'], process.cwd());
|
|
assert.strictEqual(
|
|
result.success,
|
|
true,
|
|
`state load must succeed via the host-table dispatch path; got: ${result.error}`,
|
|
);
|
|
});
|
|
|
|
test('unknown state subcommand surfaces the canonical unknown-subcommand error (non-zero exit)', async () => {
|
|
const result = runMsdTools(['--cwd=' + tmpDir, 'state', 'totally-not-a-subcommand'], process.cwd());
|
|
assert.strictEqual(result.success, false, 'unknown subcommand must exit non-zero');
|
|
assert.ok(
|
|
result.error.length > 0,
|
|
'an error message must be emitted for an unknown state subcommand',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── 5. REGISTRY — HOST_COMMAND_ROUTERS owns `state` ────────────────────────
|
|
|
|
describe('HOST_COMMAND_ROUTERS registry', async () => {
|
|
test('owns all migrated host commands as function entries (P1+P2+P3)', async () => {
|
|
const allHostCommands = [
|
|
// P1 Tier-1 host routers
|
|
'state', 'phase', 'init', 'roadmap', 'validate', 'verify',
|
|
// P2 capability router
|
|
'capability',
|
|
// P3 resolve/git/config/research routers (ADR-2346 P3)
|
|
'resolve-model', 'resolve-granularity', 'resolve-execution',
|
|
'git',
|
|
'config-ensure-section', 'config-set', 'config-set-model-profile',
|
|
'config-get', 'config-new-project', 'config-path', 'migrate-config',
|
|
'research-store', 'research-plan',
|
|
// P4 all remaining leaf commands (ADR-2346 P4)
|
|
'agent', 'smart-entry', 'check', 'find-phase', 'commit', 'check-commit',
|
|
'commit-to-subrepo', 'pr-subrepo', 'verify-summary', 'template', 'task',
|
|
'frontmatter', 'eval', 'verification', 'generate-slug', 'current-timestamp',
|
|
'project-instruction-file', 'list-todos', 'list-seeds', 'verify-path-exists',
|
|
'quick-tasks-append', 'normalize-test-command', 'dispatch-should-flatten',
|
|
'agent-skills', 'skill-manifest', 'history-digest', 'phases',
|
|
'assumption-delta', 'requirements', 'gap-analysis', 'milestone', 'progress',
|
|
'uat', 'stats', 'todo', 'scaffold', 'loop', 'phase-plan-index',
|
|
'state-snapshot', 'summary-extract', 'websearch', 'workstream', 'worktree',
|
|
'docs-init', 'learnings', 'teams-status', 'detect-custom-files',
|
|
'from-gsd2', 'prompt-budget', 'update-context', 'classify-confidence',
|
|
'package-legitimacy', 'effort', 'user-story', 'drift-guard',
|
|
];
|
|
for (const cmd of allHostCommands) {
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, cmd),
|
|
`HOST_COMMAND_ROUTERS must own \`${cmd}\``,
|
|
);
|
|
assert.strictEqual(typeof HOST_COMMAND_ROUTERS[cmd], 'function', `${cmd} entry must be a function`);
|
|
}
|
|
});
|
|
|
|
test('does NOT own prototype-pollution keys', async () => {
|
|
assert.ok(
|
|
!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, '__proto__'),
|
|
'HOST_COMMAND_ROUTERS must not own __proto__',
|
|
);
|
|
assert.ok(
|
|
!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, 'constructor'),
|
|
'HOST_COMMAND_ROUTERS must not own constructor',
|
|
);
|
|
});
|
|
});
|