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.
399 lines
16 KiB
JavaScript
399 lines
16 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* #3897 rung 2 — the per-install `.msd-runtime` marker rung in the CANONICAL
|
||
* runtime resolver (`resolveRuntime`, src/runtime-slash.cts).
|
||
*
|
||
* MINED FROM PR #3382 (closed, unmerged — "Closing unmerged — not on the
|
||
* merits."): that PR's `tests/runtime-marker-resolution.test.cjs` drove real
|
||
* `bin/install.js` installs end-to-end (6 tests). This file reuses its
|
||
* fixture/seam CONTRACT — `readInstallRuntimeMarker`, `_setInstallRuntimeMarkerForTests`,
|
||
* `_resetInstallRuntimeMarkerCacheForTests` exported from the canonical owner
|
||
* `runtime-slash.cts`, the same `neutralProject` shape (a `.planning/config.json`
|
||
* with NO `runtime` key, matching what `config-new-project` actually writes),
|
||
* and the "marker rung sits between config and the default" precedence — but
|
||
* exercises it at the resolver-unit level (in-process, monkeypatched `fs`)
|
||
* rather than through full spawned installs, per the test-matrix's rung-2
|
||
* automation note ("unit on the resolution ladder + the cache; integration
|
||
* for the hooks' delegation").
|
||
*
|
||
* Today (before #3897 rung 2 lands) `src/runtime-slash.cts` exports no marker
|
||
* reader and no test seams at all — `resolveRuntime` is a one-liner over
|
||
* `resolveExplicitRuntime(...) ?? 'claude'`. Every test below therefore fails
|
||
* at `assertMarkerSeamExists` first, which is the correct RED: the rung does
|
||
* not exist yet.
|
||
*/
|
||
|
||
const { describe, test } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||
|
||
const RUNTIME_SLASH_PATH = path.join(__dirname, '..', 'msd-core', 'bin', 'lib', 'runtime-slash.cjs');
|
||
const RUNTIME_NAME_POLICY_PATH = path.join(__dirname, '..', 'msd-core', 'bin', 'lib', 'runtime-name-policy.cjs');
|
||
const AGENT_ISOLATION_GUARD_PATH = path.join(__dirname, '..', 'hooks', 'msd-agent-isolation-guard.js');
|
||
const CURSOR_SUBAGENT_START_PATH = path.join(__dirname, '..', 'hooks', 'msd-cursor-subagent-start.js');
|
||
|
||
/** A project whose config carries NO `runtime` key — what config-new-project writes. */
|
||
function neutralProject(t) {
|
||
const dir = createTempProject('msd-marker-proj-');
|
||
t.after(() => cleanup(dir));
|
||
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{}');
|
||
return dir;
|
||
}
|
||
|
||
/** Fresh require of the built lib, bypassing require() cache so each test sees current disk state. */
|
||
function requireRuntimeSlash() {
|
||
delete require.cache[require.resolve(RUNTIME_SLASH_PATH)];
|
||
return require(RUNTIME_SLASH_PATH);
|
||
}
|
||
|
||
/**
|
||
* Guard clause every test opens with: the marker rung's seam surface must
|
||
* exist on the CANONICAL owner before any scenario can be exercised. On the
|
||
* current tree this assertion itself is the RED — `runtime-slash.cjs` has no
|
||
* marker reader and no test seams.
|
||
*/
|
||
function assertMarkerSeamExists(rs) {
|
||
assert.equal(
|
||
typeof rs.readInstallRuntimeMarker,
|
||
'function',
|
||
'runtime-slash.cjs must export readInstallRuntimeMarker — the canonical marker reader (#3897 rung 2) does not exist yet',
|
||
);
|
||
assert.equal(
|
||
typeof rs._setInstallRuntimeMarkerForTests,
|
||
'function',
|
||
'runtime-slash.cjs must export _setInstallRuntimeMarkerForTests — the test seam for the marker rung does not exist yet',
|
||
);
|
||
assert.equal(
|
||
typeof rs._resetInstallRuntimeMarkerCacheForTests,
|
||
'function',
|
||
'runtime-slash.cjs must export _resetInstallRuntimeMarkerCacheForTests',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Run `fn` with `overrides` applied to process.env, restoring exactly what
|
||
* was there before. An override value of `undefined` DELETES that key for
|
||
* the duration of `fn` (never `Object.assign`s it — assigning `undefined`
|
||
* onto `process.env` coerces to the literal string `"undefined"`, not a
|
||
* deletion) — #3897 rung 4 (isolated correctness review, NIT finding 8b):
|
||
* every `delete process.env.MSD_RUNTIME` in this file used to bypass this
|
||
* helper entirely and never restore the ambient value afterward, leaking a
|
||
* forced-unset MSD_RUNTIME into whatever test (in this file or a later one
|
||
* sharing the same process) ran next. Every test below now routes through
|
||
* this helper, `undefined`-keyed, so ambient env state is always restored.
|
||
*/
|
||
function withEnv(overrides, fn) {
|
||
const saved = {};
|
||
for (const key of Object.keys(overrides)) saved[key] = process.env[key];
|
||
for (const key of Object.keys(overrides)) {
|
||
const value = overrides[key];
|
||
if (value === undefined) delete process.env[key];
|
||
else process.env[key] = value;
|
||
}
|
||
try {
|
||
return fn();
|
||
} finally {
|
||
for (const key of Object.keys(overrides)) {
|
||
if (saved[key] === undefined) delete process.env[key];
|
||
else process.env[key] = saved[key];
|
||
}
|
||
}
|
||
}
|
||
|
||
describe('per-install .msd-runtime marker rung in the canonical resolver (#3897 rung 2)', () => {
|
||
test('T3 installMarkerResolvesWhenEnvAndConfigAbsent_3897 (#3364)', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
const proj = neutralProject(t);
|
||
rs._setInstallRuntimeMarkerForTests('codex');
|
||
t.after(() => rs._resetInstallRuntimeMarkerCacheForTests());
|
||
|
||
withEnv({ MSD_RUNTIME: undefined }, () => {
|
||
assert.equal(
|
||
rs.resolveRuntime(proj),
|
||
'codex',
|
||
'no env, no config.runtime, marker says codex (R3) — resolveRuntime must consult the marker instead of falling straight to claude',
|
||
);
|
||
});
|
||
});
|
||
|
||
test('T1 envRuntimeOutranksConfigAndMarker', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
const proj = neutralProject(t);
|
||
fs.writeFileSync(path.join(proj, '.planning', 'config.json'), JSON.stringify({ runtime: 'cursor' }));
|
||
rs._setInstallRuntimeMarkerForTests('gemini');
|
||
t.after(() => rs._resetInstallRuntimeMarkerCacheForTests());
|
||
|
||
withEnv({ MSD_RUNTIME: 'codex' }, () => {
|
||
assert.equal(rs.resolveRuntime(proj), 'codex', 'MSD_RUNTIME must outrank both config.runtime and the install marker');
|
||
});
|
||
});
|
||
|
||
test('T2 configOutranksInstallMarker', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
const proj = neutralProject(t);
|
||
fs.writeFileSync(path.join(proj, '.planning', 'config.json'), JSON.stringify({ runtime: 'cursor' }));
|
||
rs._setInstallRuntimeMarkerForTests('codex');
|
||
t.after(() => rs._resetInstallRuntimeMarkerCacheForTests());
|
||
|
||
withEnv({ MSD_RUNTIME: undefined }, () => {
|
||
assert.equal(rs.resolveRuntime(proj), 'cursor', 'config.runtime must outrank the install marker');
|
||
});
|
||
});
|
||
|
||
test('T4 noSignalsFallsBackToClaude', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
const proj = neutralProject(t);
|
||
rs._setInstallRuntimeMarkerForTests(null);
|
||
t.after(() => rs._resetInstallRuntimeMarkerCacheForTests());
|
||
|
||
withEnv({ MSD_RUNTIME: undefined }, () => {
|
||
assert.equal(rs.resolveRuntime(proj), 'claude', 'with no env, no config.runtime and no marker, the default is unchanged');
|
||
});
|
||
});
|
||
|
||
test('T5 emptyMarkerFallsThrough', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
rs._resetInstallRuntimeMarkerCacheForTests();
|
||
const originalReadFileSync = fs.readFileSync;
|
||
fs.readFileSync = (p, enc) => {
|
||
if (typeof p === 'string' && p.endsWith('.msd-runtime')) return ' \n\t ';
|
||
return originalReadFileSync(p, enc);
|
||
};
|
||
t.after(() => {
|
||
fs.readFileSync = originalReadFileSync;
|
||
rs._resetInstallRuntimeMarkerCacheForTests();
|
||
});
|
||
|
||
assert.equal(
|
||
rs.readInstallRuntimeMarker(),
|
||
null,
|
||
'a whitespace-only marker file must resolve to null (trimmed to empty), not an empty-string runtime name',
|
||
);
|
||
|
||
const proj = neutralProject(t);
|
||
withEnv({ MSD_RUNTIME: undefined }, () => {
|
||
assert.equal(rs.resolveRuntime(proj), 'claude', 'an empty marker must fall through to claude');
|
||
});
|
||
});
|
||
|
||
test('T6 unreadableMarkerNeverThrows', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
rs._resetInstallRuntimeMarkerCacheForTests();
|
||
const originalReadFileSync = fs.readFileSync;
|
||
// IO failure injected by monkeypatching fs, never chmod (root bypasses
|
||
// mode bits — a chmod test would pass with zero coverage in root CI).
|
||
fs.readFileSync = (p, enc) => {
|
||
if (typeof p === 'string' && p.endsWith('.msd-runtime')) {
|
||
const err = new Error('EACCES: permission denied, open .msd-runtime');
|
||
err.code = 'EACCES';
|
||
throw err;
|
||
}
|
||
return originalReadFileSync(p, enc);
|
||
};
|
||
t.after(() => {
|
||
fs.readFileSync = originalReadFileSync;
|
||
rs._resetInstallRuntimeMarkerCacheForTests();
|
||
});
|
||
|
||
assert.doesNotThrow(
|
||
() => rs.readInstallRuntimeMarker(),
|
||
'an EACCES marker read must never throw out of readInstallRuntimeMarker — a hook that throws breaks the session (N4)',
|
||
);
|
||
assert.equal(rs.readInstallRuntimeMarker(), null);
|
||
|
||
const proj = neutralProject(t);
|
||
let result;
|
||
withEnv({ MSD_RUNTIME: undefined }, () => {
|
||
assert.doesNotThrow(() => {
|
||
result = rs.resolveRuntime(proj);
|
||
});
|
||
});
|
||
assert.equal(result, 'claude', 'an unreadable marker must fall through to claude, never crash the caller');
|
||
});
|
||
|
||
test('T7 unknownMarkerNameIsNormalizedNotTrusted', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
const proj = neutralProject(t);
|
||
rs._setInstallRuntimeMarkerForTests('NotARealRuntime-V9');
|
||
t.after(() => rs._resetInstallRuntimeMarkerCacheForTests());
|
||
|
||
const viaMarker = withEnv({ MSD_RUNTIME: undefined }, () => rs.resolveRuntime(proj));
|
||
const viaEnv = withEnv({ MSD_RUNTIME: 'NotARealRuntime-V9' }, () => rs.resolveRuntime(neutralProject(t)));
|
||
|
||
assert.equal(
|
||
viaMarker,
|
||
viaEnv,
|
||
'the marker rung must apply the SAME resolveRuntimeNameFromCandidates normalization MSD_RUNTIME does (N1) — one name policy, not two',
|
||
);
|
||
assert.notEqual(
|
||
viaMarker,
|
||
'NotARealRuntime-V9',
|
||
'the raw marker string must never be returned verbatim — it must be routed through normalization like the env rung',
|
||
);
|
||
});
|
||
|
||
test('T8 hostileMarkerContentIsInert', (t) => {
|
||
const rs = requireRuntimeSlash();
|
||
assertMarkerSeamExists(rs);
|
||
rs._resetInstallRuntimeMarkerCacheForTests();
|
||
const hostile = 'codex\n../../etc/passwd |