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:
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
/**
|
||||
* E2E content tests for the GSD capability engine — ADR-857 phase 6
|
||||
* E2E content tests for the MSD capability engine — ADR-857 phase 6
|
||||
*
|
||||
* Hook points tested: discuss:pre, discuss:post, execute:pre, execute:wave:pre,
|
||||
* verify:pre, ship:post
|
||||
@@ -30,15 +30,15 @@ const { spawnSync } = require('node:child_process');
|
||||
const { cleanup, installSpawnEnv } = require('./helpers.cjs');
|
||||
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
const { resolveLoopHooks, renderLoopHooks } = require('../gsd-core/bin/lib/loop-resolver.cjs');
|
||||
const MSD_TOOLS = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
|
||||
const realRegistry = require('../msd-core/bin/lib/capability-registry.cjs');
|
||||
const { resolveLoopHooks, renderLoopHooks } = require('../msd-core/bin/lib/loop-resolver.cjs');
|
||||
|
||||
// ─── Fixture helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Create a bare temp dir with .planning/ layout (no config.json) */
|
||||
function makeTempProject() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-loop-e2e-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-loop-e2e-'));
|
||||
fs.mkdirSync(path.join(dir, '.planning', 'phases'), { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
@@ -52,21 +52,21 @@ function makeTempProjectWithConfig(configObj) {
|
||||
|
||||
/** Create a bare temp dir with no .planning directory at all */
|
||||
function makeBareDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-loop-bare-'));
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'msd-loop-bare-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn gsd-tools via raw spawnSync; returns { status, stdout, stderr }.
|
||||
* Spawn msd-tools via raw spawnSync; returns { status, stdout, stderr }.
|
||||
*
|
||||
* #4291: env: installSpawnEnv() sandboxes HOME AND blanks GSD_HOME, which
|
||||
* #4291: env: installSpawnEnv() sandboxes HOME AND blanks MSD_HOME, which
|
||||
* capability-loader's overlayRoots checks BEFORE falling back to
|
||||
* os.homedir() — sandboxing HOME alone would leave that env var reaching
|
||||
* the real machine. Without both, a capability genuinely installed on the
|
||||
* host running the suite (e.g. beads, markdown-linting) leaked into these
|
||||
* empty-point/exact-shape assertions.
|
||||
*/
|
||||
function spawnGsd(args, cwd) {
|
||||
return spawnSync(process.execPath, [GSD_TOOLS, ...args], {
|
||||
function spawnMsd(args, cwd) {
|
||||
return spawnSync(process.execPath, [MSD_TOOLS, ...args], {
|
||||
cwd: cwd || os.tmpdir(),
|
||||
encoding: 'utf8',
|
||||
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
|
||||
@@ -132,7 +132,7 @@ describe('discuss:pre — real registry empty-resolution', () => {
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
it('[happy] discuss:pre with real registry returns exact 3-key envelope with empty activeHooks (Gall\'s Law E2E pin)', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:pre', '--cwd', tmpDir, '--raw'], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:pre', '--cwd', tmpDir, '--raw'], tmpDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0, got ${result.status}. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'discuss:pre');
|
||||
@@ -144,7 +144,7 @@ describe('discuss:pre — real registry empty-resolution', () => {
|
||||
it('[bva] discuss:pre with all capability config keys enabled still returns activeHooks:[] — config does not activate phantom hooks', () => {
|
||||
const configDir = makeTempProjectWithConfig(ALL_CAPS_ON_CONFIG);
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:pre', '--cwd', configDir, '--raw'], configDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:pre', '--cwd', configDir, '--raw'], configDir);
|
||||
assert.strictEqual(result.status, 0);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.deepEqual(envelope.activeHooks, [], 'No capability config should activate hooks at discuss:pre');
|
||||
@@ -166,14 +166,14 @@ describe('discuss:pre — real registry empty-resolution', () => {
|
||||
});
|
||||
|
||||
it('[negative] discuss:pre missing-point argument to CLI exits non-zero with clear message', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', '--raw'], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', '--raw'], tmpDir);
|
||||
assert.notStrictEqual(result.status, 0, 'must exit non-zero when point arg is missing');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /render-hooks requires a .point. argument/i);
|
||||
});
|
||||
|
||||
it('[bva] discuss:pre close-typo "discuss:pre " (trailing space) exits non-zero — boundary for point name validation', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:pre ', '--raw'], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:pre ', '--raw'], tmpDir);
|
||||
assert.notStrictEqual(result.status, 0, 'must exit non-zero for invalid point');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /Invalid loop point/i);
|
||||
@@ -192,7 +192,7 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
it('[empty-resolution] discuss:post E2E subprocess returns exact empty envelope — activeHooks:[], 3-key shape, placeholder string pinned', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'discuss:post');
|
||||
@@ -205,7 +205,7 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
|
||||
it('[happy] discuss:post E2E with no .planning directory returns empty hooks (Postel leniency path)', () => {
|
||||
const bareDir = makeBareDir();
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', bareDir], bareDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', bareDir], bareDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0 even with no .planning dir. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.activeHooks.length, 0);
|
||||
@@ -268,7 +268,7 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
|
||||
});
|
||||
|
||||
it('[negative] discuss:post with invalid point name "discuss:past" exits non-zero and lists valid points', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:past', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:past', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
assert.notStrictEqual(result.status, 0, 'must exit non-zero for typo point name');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /discuss:past|Invalid loop point/i);
|
||||
@@ -279,7 +279,7 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
|
||||
const malformedDir = makeTempProject();
|
||||
fs.writeFileSync(path.join(malformedDir, '.planning', 'config.json'), '{invalid json');
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', malformedDir], malformedDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:post', '--raw', '--cwd', malformedDir], malformedDir);
|
||||
assert.strictEqual(result.status, 0, `must not crash on malformed config. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.activeHooks.length, 0);
|
||||
@@ -298,7 +298,7 @@ describe('execute:pre — real registry empty-resolution + synthetic resolver me
|
||||
it('[happy] execute:pre with real registry + all capability flags enabled returns 0 active hooks and exact placeholder', () => {
|
||||
const allOnDir = makeTempProjectWithConfig(ALL_CAPS_ON_CONFIG);
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'execute:pre', '--raw', '--cwd', allOnDir], allOnDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'execute:pre', '--raw', '--cwd', allOnDir], allOnDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
|
||||
const parsed = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(parsed.point, 'execute:pre');
|
||||
@@ -312,7 +312,7 @@ describe('execute:pre — real registry empty-resolution + synthetic resolver me
|
||||
it('[empty-resolution] execute:pre with no config.json returns 0 active hooks (empty-project negative space)', () => {
|
||||
const emptyDir = makeTempProject();
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'execute:pre', '--raw', '--cwd', emptyDir], emptyDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'execute:pre', '--raw', '--cwd', emptyDir], emptyDir);
|
||||
assert.strictEqual(result.status, 0);
|
||||
const parsed = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(parsed.activeHooks.length, 0);
|
||||
@@ -418,7 +418,7 @@ describe('execute:pre — real registry empty-resolution + synthetic resolver me
|
||||
|
||||
describe('execute:wave:pre — real registry empty-resolution + synthetic mechanics', () => {
|
||||
it('[empty-resolution] execute:wave:pre with real registry returns empty activeHooks and exact placeholder text', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'execute:wave:pre', '--raw'], os.tmpdir());
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'execute:wave:pre', '--raw'], os.tmpdir());
|
||||
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'execute:wave:pre');
|
||||
@@ -522,7 +522,7 @@ describe('verify:pre — real registry carries the api-coverage gate (#1562); sy
|
||||
});
|
||||
|
||||
it('[happy] verify:pre E2E subprocess returns a well-formed envelope carrying the gate (Hyrum\'s Law contract pin)', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'verify:pre', '--cwd', tmpEmptyProjectDir, '--raw'], tmpEmptyProjectDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'verify:pre', '--cwd', tmpEmptyProjectDir, '--raw'], tmpEmptyProjectDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'verify:pre');
|
||||
@@ -581,7 +581,7 @@ describe('verify:pre — real registry carries the api-coverage gate (#1562); sy
|
||||
const malformedDir = makeTempProject();
|
||||
fs.writeFileSync(path.join(malformedDir, '.planning', 'config.json'), '{invalid json');
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'verify:pre', '--cwd', malformedDir, '--raw'], malformedDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'verify:pre', '--cwd', malformedDir, '--raw'], malformedDir);
|
||||
assert.strictEqual(result.status, 0, `must not crash on malformed config. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
// Malformed config degrades to defaults (gate default-on); the contract
|
||||
@@ -602,7 +602,7 @@ describe('ship:post — real registry empty-resolution + resilience to registry
|
||||
it('[happy] ship:post E2E subprocess returns typed envelope: point=\'ship:post\', activeHooks=[], rendered=placeholder, no extra keys', () => {
|
||||
const tmpDir = makeTempProject();
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', tmpDir], tmpDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'ship:post');
|
||||
@@ -618,7 +618,7 @@ describe('ship:post — real registry empty-resolution + resilience to registry
|
||||
it('[bva] ship:post returns empty activeHooks regardless of any capability config being enabled — no config leaks hooks into this point', () => {
|
||||
const allOnDir = makeTempProjectWithConfig(ALL_CAPS_ON_CONFIG);
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', allOnDir], allOnDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', allOnDir], allOnDir);
|
||||
assert.strictEqual(result.status, 0);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.deepEqual(envelope.activeHooks, [], 'Maximum capability activation must still produce zero hooks at ship:post');
|
||||
@@ -645,7 +645,7 @@ describe('ship:post — real registry empty-resolution + resilience to registry
|
||||
it('[negative] ship:post E2E exits 0 and returns empty envelope when project has no .planning directory at all', () => {
|
||||
const bareDir = makeBareDir();
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', bareDir], bareDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', bareDir], bareDir);
|
||||
assert.strictEqual(result.status, 0, `expected exit 0 even with no .planning dir. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.activeHooks.length, 0);
|
||||
@@ -693,13 +693,13 @@ describe('ship:post — real registry empty-resolution + resilience to registry
|
||||
const securityOnDir = makeTempProjectWithConfig({ workflow: { security_enforcement: true } });
|
||||
try {
|
||||
// ship:post must be empty
|
||||
const postResult = spawnGsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', securityOnDir], securityOnDir);
|
||||
const postResult = spawnMsd(['loop', 'render-hooks', 'ship:post', '--raw', '--cwd', securityOnDir], securityOnDir);
|
||||
assert.strictEqual(postResult.status, 0);
|
||||
const postEnvelope = JSON.parse(postResult.stdout.trim());
|
||||
assert.deepEqual(postEnvelope.activeHooks, [], 'ship:post must be empty even with security_enforcement=true');
|
||||
|
||||
// ship:pre must have the security gate (proves the config actually works and the difference is real)
|
||||
const preResult = spawnGsd(['loop', 'render-hooks', 'ship:pre', '--raw', '--cwd', securityOnDir], securityOnDir);
|
||||
const preResult = spawnMsd(['loop', 'render-hooks', 'ship:pre', '--raw', '--cwd', securityOnDir], securityOnDir);
|
||||
assert.strictEqual(preResult.status, 0);
|
||||
const preEnvelope = JSON.parse(preResult.stdout.trim());
|
||||
const secGate = preEnvelope.activeHooks.find(h => h.capId === 'security');
|
||||
@@ -725,28 +725,28 @@ describe('ship:post — real registry empty-resolution + resilience to registry
|
||||
|
||||
describe('CLI contract — missing/invalid point argument (shared across all 6 empty points)', () => {
|
||||
it('[negative] missing point argument exits non-zero and includes render-hooks syntax in error message', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', '--raw'], os.tmpdir());
|
||||
const result = spawnMsd(['loop', 'render-hooks', '--raw'], os.tmpdir());
|
||||
assert.notStrictEqual(result.status, 0, 'must exit non-zero when point arg is missing');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /render-hooks requires a .point. argument/i);
|
||||
});
|
||||
|
||||
it('[bva] "discuss:post " with trailing space exits non-zero — boundary: trailing whitespace makes point invalid', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'discuss:post ', '--raw'], os.tmpdir());
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'discuss:post ', '--raw'], os.tmpdir());
|
||||
assert.notStrictEqual(result.status, 0, 'must reject point with trailing space');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /Invalid loop point/i);
|
||||
});
|
||||
|
||||
it('[bva] "execute:pre." with trailing period exits non-zero — boundary: period suffix makes point invalid', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'execute:pre.', '--raw'], os.tmpdir());
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'execute:pre.', '--raw'], os.tmpdir());
|
||||
assert.notStrictEqual(result.status, 0, 'must reject point with trailing period');
|
||||
const combined = (result.stdout + result.stderr);
|
||||
assert.match(combined, /Invalid loop point/i);
|
||||
});
|
||||
|
||||
it('[negative] error message for invalid point includes list of valid points so callers can self-correct', () => {
|
||||
const result = spawnGsd(['loop', 'render-hooks', 'verify:future', '--raw'], os.tmpdir());
|
||||
const result = spawnMsd(['loop', 'render-hooks', 'verify:future', '--raw'], os.tmpdir());
|
||||
assert.notStrictEqual(result.status, 0);
|
||||
const combined = (result.stdout + result.stderr);
|
||||
// Must include at least several valid points in the error message
|
||||
@@ -772,7 +772,7 @@ describe('Parametric E2E sweep — 4 empty points return correct envelope shape
|
||||
it(`[parametric] ${point} — E2E subprocess exits 0 with {point, activeHooks:[], rendered:placeholder}`, () => {
|
||||
const tmpDir = makeTempProject();
|
||||
try {
|
||||
const result = spawnGsd(['loop', 'render-hooks', point, '--raw', '--cwd', tmpDir], tmpDir);
|
||||
const result = spawnMsd(['loop', 'render-hooks', point, '--raw', '--cwd', tmpDir], tmpDir);
|
||||
assert.strictEqual(result.status, 0, `${point}: expected exit 0. stderr: ${result.stderr}`);
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, point, `${point}: envelope.point mismatch`);
|
||||
|
||||
Reference in New Issue
Block a user