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,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* gsd-cursor-subagent-start.js — Cursor subagentStart isolation guard (#3045)
|
||||
* msd-cursor-subagent-start.js — Cursor subagentStart isolation guard (#3045)
|
||||
*
|
||||
* Seam: hooks/gsd-cursor-subagent-start.js (Cursor `subagentStart` hook,
|
||||
* Seam: hooks/msd-cursor-subagent-start.js (Cursor `subagentStart` hook,
|
||||
* spawned with a JSON payload on stdin, exactly as Cursor's hook bus invokes
|
||||
* it). This file covers the NEW isolation-enforcement behavior added
|
||||
* alongside the pre-existing #2587 workspace-reminder behavior (that
|
||||
@@ -16,7 +16,7 @@
|
||||
* workspace root actually a linked git worktree / under Cursor's managed
|
||||
* worktree root) rather than checking for a flag on the call.
|
||||
*
|
||||
* Output contract differs from hooks/gsd-agent-isolation-guard.js's Claude
|
||||
* Output contract differs from hooks/msd-agent-isolation-guard.js's Claude
|
||||
* contract ({decision:'block'} + exit 2): this hook always exits 0 and
|
||||
* communicates via stdout JSON {permission, user_message} — asserted
|
||||
* precisely below because getting this wrong means the guard silently never
|
||||
@@ -34,11 +34,11 @@ const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { gitOrThrow, toLegacyResult } = require('./helpers/git-fixture.cjs');
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-cursor-subagent-start.js');
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'msd-cursor-subagent-start.js');
|
||||
|
||||
/**
|
||||
* Write a #3045 dispatch-isolation sentinel under `dir` (mirrors what
|
||||
* `gsd-tools.cjs record-dispatch-isolation` writes). `writtenAt` defaults to
|
||||
* `msd-tools.cjs record-dispatch-isolation` writes). `writtenAt` defaults to
|
||||
* "now" (fresh); pass an explicit past timestamp to construct a stale one.
|
||||
*/
|
||||
function writeSentinel(dir, { isolation, harnessFlag = null, phase = null, plan = null, writtenAt = Date.now() }) {
|
||||
@@ -49,7 +49,7 @@ function writeSentinel(dir, { isolation, harnessFlag = null, phase = null, plan
|
||||
|
||||
function runHook(payload, extraEnv = {}) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_RUNTIME;
|
||||
delete env.MSD_RUNTIME;
|
||||
delete env.CURSOR_CONFIG_DIR;
|
||||
Object.assign(env, extraEnv);
|
||||
// Production code resolves the home directory via `os.homedir()` (correct,
|
||||
@@ -74,7 +74,7 @@ function subagentPayload(workspaceRoots, overrides = {}) {
|
||||
generation_id: 'gen-1',
|
||||
workspace_roots: workspaceRoots,
|
||||
subagent_id: 'sub-1',
|
||||
subagent_type: 'gsd-executor',
|
||||
subagent_type: 'msd-executor',
|
||||
task: 'do the thing',
|
||||
parent_conversation_id: 'conv-0',
|
||||
tool_call_id: 'call-1',
|
||||
@@ -102,36 +102,36 @@ function makeGitProject(prefix, configContent) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)', () => {
|
||||
describe('msd-cursor-subagent-start.js: isolation guard applicability (#3045)', () => {
|
||||
let harnessProject; // real git repo, config resolves to harness-worktree (default 'cursor')
|
||||
let linkedWorktree; // real `git worktree add` of harnessProject, NOT under Cursor's managed
|
||||
// worktree root — a hand-made worktree the user opened themselves.
|
||||
// #3045 finding 3: this is no longer treated as isolation proof.
|
||||
let noneProject; // config.json { runtime: 'windsurf' } -> resolves to 'none'
|
||||
let orchestratorEnvProject; // exercised via GSD_RUNTIME=codex -> 'orchestrator-worktree'
|
||||
let noGsdDir; // no .planning at all
|
||||
let orchestratorEnvProject; // exercised via MSD_RUNTIME=codex -> 'orchestrator-worktree'
|
||||
let noMsdDir; // no .planning at all
|
||||
let unreadableConfigProject; // config.json is a directory (EISDIR)
|
||||
|
||||
before(() => {
|
||||
// #3045 MAJOR fix ("Cursor residual false-deny"): the fallback resolver
|
||||
// no longer defaults confidently to 'cursor' when config.json carries no
|
||||
// `runtime` key (see hooks/gsd-cursor-subagent-start.js's
|
||||
// `runtime` key (see hooks/msd-cursor-subagent-start.js's
|
||||
// resolveFallbackIsolation doc comment) — an explicit signal is now
|
||||
// required. This fixture intentionally declares one so the REST of this
|
||||
// describe block still exercises a properly-configured Cursor+GSD
|
||||
// describe block still exercises a properly-configured Cursor+MSD
|
||||
// project resolving harness-worktree, not the newly-inert unconfigured
|
||||
// case (covered separately below).
|
||||
harnessProject = makeGitProject('gsd-cs-harness-', JSON.stringify({ runtime: 'cursor' }));
|
||||
const linkedPath = path.join(fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'gsd-cs-wt-parent-')), 'linked');
|
||||
git(['worktree', 'add', linkedPath, '-b', 'agent-gsd-cs-iso-test'], harnessProject);
|
||||
harnessProject = makeGitProject('msd-cs-harness-', JSON.stringify({ runtime: 'cursor' }));
|
||||
const linkedPath = path.join(fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'msd-cs-wt-parent-')), 'linked');
|
||||
git(['worktree', 'add', linkedPath, '-b', 'agent-msd-cs-iso-test'], harnessProject);
|
||||
linkedWorktree = linkedPath;
|
||||
|
||||
noneProject = makeGitProject('gsd-cs-none-', JSON.stringify({ runtime: 'windsurf' }));
|
||||
orchestratorEnvProject = makeGitProject('gsd-cs-orch-', JSON.stringify({}));
|
||||
noneProject = makeGitProject('msd-cs-none-', JSON.stringify({ runtime: 'windsurf' }));
|
||||
orchestratorEnvProject = makeGitProject('msd-cs-orch-', JSON.stringify({}));
|
||||
|
||||
noGsdDir = createTempDir('gsd-cs-nogsd-');
|
||||
noMsdDir = createTempDir('msd-cs-nomsd-');
|
||||
|
||||
unreadableConfigProject = createTempDir('gsd-cs-unreadable-');
|
||||
unreadableConfigProject = createTempDir('msd-cs-unreadable-');
|
||||
fs.mkdirSync(path.join(unreadableConfigProject, '.planning', 'config.json'), { recursive: true });
|
||||
});
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
cleanup(path.dirname(linkedWorktree));
|
||||
cleanup(noneProject);
|
||||
cleanup(orchestratorEnvProject);
|
||||
cleanup(noGsdDir);
|
||||
cleanup(noMsdDir);
|
||||
cleanup(unreadableConfigProject);
|
||||
});
|
||||
|
||||
@@ -177,8 +177,8 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
assert.equal(out.permission, undefined);
|
||||
});
|
||||
|
||||
test('resolved mode orchestrator-worktree (GSD_RUNTIME=codex) -> allow (different path)', () => {
|
||||
const r = runHook(subagentPayload([orchestratorEnvProject]), { GSD_RUNTIME: 'codex' });
|
||||
test('resolved mode orchestrator-worktree (MSD_RUNTIME=codex) -> allow (different path)', () => {
|
||||
const r = runHook(subagentPayload([orchestratorEnvProject]), { MSD_RUNTIME: 'codex' });
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout}`);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, undefined);
|
||||
@@ -191,8 +191,8 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
assert.equal(out.permission, undefined);
|
||||
});
|
||||
|
||||
test('no GSD project (.planning/config.json absent) -> allow, inert', () => {
|
||||
const r = runHook(subagentPayload([noGsdDir]));
|
||||
test('no MSD project (.planning/config.json absent) -> allow, inert', () => {
|
||||
const r = runHook(subagentPayload([noMsdDir]));
|
||||
assert.equal(r.status, 0);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, undefined);
|
||||
@@ -217,7 +217,7 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
assert.equal(out.permission, undefined);
|
||||
});
|
||||
|
||||
test('subagent_type entirely absent, harness-worktree GSD project -> DENY (cannot determine)', () => {
|
||||
test('subagent_type entirely absent, harness-worktree MSD project -> DENY (cannot determine)', () => {
|
||||
const payload = subagentPayload([harnessProject]);
|
||||
delete payload.subagent_type;
|
||||
const r = runHook(payload);
|
||||
@@ -238,7 +238,7 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
});
|
||||
|
||||
test('malformed subagent_type (non-string) in a harness-worktree project -> DENY (cannot determine)', () => {
|
||||
const r = runHook(subagentPayload([harnessProject], { subagent_type: ['gsd-executor'] }));
|
||||
const r = runHook(subagentPayload([harnessProject], { subagent_type: ['msd-executor'] }));
|
||||
assert.equal(r.status, 0);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, 'deny');
|
||||
@@ -268,14 +268,14 @@ describe('gsd-cursor-subagent-start.js: isolation guard applicability (#3045)',
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: managed-worktree-root OR-signal (#3045)', () => {
|
||||
describe('msd-cursor-subagent-start.js: managed-worktree-root OR-signal (#3045)', () => {
|
||||
let cursorConfigDir;
|
||||
let managedWorktree;
|
||||
|
||||
before(() => {
|
||||
cursorConfigDir = createTempDir('gsd-cs-cursorhome-');
|
||||
cursorConfigDir = createTempDir('msd-cs-cursorhome-');
|
||||
// Not a git repo at all — a plain directory under
|
||||
// <CURSOR_CONFIG_DIR>/worktrees, with its own harness-worktree GSD
|
||||
// <CURSOR_CONFIG_DIR>/worktrees, with its own harness-worktree MSD
|
||||
// project. resolveWorktreeLinkage alone would report 'not_git_repo'
|
||||
// (a confident negative); the managed-root signal must still ALLOW.
|
||||
managedWorktree = path.join(cursorConfigDir, 'worktrees', 'agent-1');
|
||||
@@ -299,7 +299,7 @@ describe('gsd-cursor-subagent-start.js: managed-worktree-root OR-signal (#3045)'
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: multi-root workspace scan (#3045 finding 1)', () => {
|
||||
describe('msd-cursor-subagent-start.js: multi-root workspace scan (#3045 finding 1)', () => {
|
||||
let unisolatedProject; // real git repo, harness-worktree, main checkout (not isolated)
|
||||
let benignRoot; // a plain directory with no .planning/ at all
|
||||
let cursorConfigDir;
|
||||
@@ -308,10 +308,10 @@ describe('gsd-cursor-subagent-start.js: multi-root workspace scan (#3045 finding
|
||||
before(() => {
|
||||
// #3045 MAJOR fix: explicit runtime signal required — see the note on
|
||||
// the first `harnessProject` fixture above.
|
||||
unisolatedProject = makeGitProject('gsd-cs-multiroot-primary-', JSON.stringify({ runtime: 'cursor' }));
|
||||
benignRoot = createTempDir('gsd-cs-multiroot-benign-');
|
||||
unisolatedProject = makeGitProject('msd-cs-multiroot-primary-', JSON.stringify({ runtime: 'cursor' }));
|
||||
benignRoot = createTempDir('msd-cs-multiroot-benign-');
|
||||
|
||||
cursorConfigDir = createTempDir('gsd-cs-multiroot-cursorhome-');
|
||||
cursorConfigDir = createTempDir('msd-cs-multiroot-cursorhome-');
|
||||
managedWorktree = path.join(cursorConfigDir, 'worktrees', 'agent-1');
|
||||
fs.mkdirSync(managedWorktree, { recursive: true });
|
||||
});
|
||||
@@ -322,9 +322,9 @@ describe('gsd-cursor-subagent-start.js: multi-root workspace scan (#3045 finding
|
||||
cleanup(cursorConfigDir);
|
||||
});
|
||||
|
||||
test('root[0] is a benign non-GSD directory, root[1] is the unisolated primary checkout -> DENY', () => {
|
||||
test('root[0] is a benign non-MSD directory, root[1] is the unisolated primary checkout -> DENY', () => {
|
||||
// Prior to the fix, firstWorkspaceRoot() only ever looked at
|
||||
// workspace_roots[0] — a non-GSD root[0] made the guard evaluate nothing
|
||||
// workspace_roots[0] — a non-MSD root[0] made the guard evaluate nothing
|
||||
// and allow, even though root[1] is the exact project this guard exists
|
||||
// to protect.
|
||||
const r = runHook(subagentPayload([benignRoot, unisolatedProject]), { CURSOR_CONFIG_DIR: cursorConfigDir });
|
||||
@@ -354,17 +354,17 @@ describe('gsd-cursor-subagent-start.js: multi-root workspace scan (#3045 finding
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: realpath verification against symlink/bind-mount spoofing (#3045 finding 2)', () => {
|
||||
describe('msd-cursor-subagent-start.js: realpath verification against symlink/bind-mount spoofing (#3045 finding 2)', () => {
|
||||
let cursorConfigDir;
|
||||
let primaryCheckout; // real git repo, harness-worktree, main checkout (not isolated)
|
||||
let spoofedManagedPath; // <CURSOR_CONFIG_DIR>/worktrees/<name>, a SYMLINK to primaryCheckout
|
||||
let symlinkError = null; // set when fs.symlinkSync('dir') fails (unprivileged Windows)
|
||||
|
||||
before(() => {
|
||||
cursorConfigDir = createTempDir('gsd-cs-symlink-cursorhome-');
|
||||
cursorConfigDir = createTempDir('msd-cs-symlink-cursorhome-');
|
||||
// #3045 MAJOR fix: explicit runtime signal required — see the note on
|
||||
// the first `harnessProject` fixture above.
|
||||
primaryCheckout = makeGitProject('gsd-cs-symlink-primary-', JSON.stringify({ runtime: 'cursor' }));
|
||||
primaryCheckout = makeGitProject('msd-cs-symlink-primary-', JSON.stringify({ runtime: 'cursor' }));
|
||||
fs.mkdirSync(path.join(cursorConfigDir, 'worktrees'), { recursive: true });
|
||||
spoofedManagedPath = path.join(cursorConfigDir, 'worktrees', 'spoofed');
|
||||
// Creating a DIRECTORY symlink requires elevated privileges (or Developer
|
||||
@@ -405,12 +405,12 @@ describe('gsd-cursor-subagent-start.js: realpath verification against symlink/bi
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof coverage, no symlink required (#3045 finding 2, Windows-safe)', () => {
|
||||
describe('msd-cursor-subagent-start.js: realpath seam — in-process spoof coverage, no symlink required (#3045 finding 2, Windows-safe)', () => {
|
||||
// Same threat model as the symlink describe block above (a path planted at
|
||||
// <CURSOR_CONFIG_DIR>/worktrees/<name> pointing OUTSIDE the managed root,
|
||||
// at the user's primary checkout, must not be classified as isolated) but
|
||||
// exercised entirely in-process via the `realpath` dependency-injection
|
||||
// seam added to hooks/gsd-cursor-subagent-start.js's
|
||||
// seam added to hooks/msd-cursor-subagent-start.js's
|
||||
// resolveIsolationEvidence/evaluateRootIsolation. No fs.symlinkSync call —
|
||||
// runs identically, unprivileged, on Windows/macOS/Linux.
|
||||
//
|
||||
@@ -424,7 +424,7 @@ describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof cover
|
||||
// being lexically nested under the managed root) and instead follows the
|
||||
// (simulated) symlink-resolved target, exactly as a real spoof would need
|
||||
// to be defeated.
|
||||
const cursorHookModule = require('../hooks/gsd-cursor-subagent-start.js');
|
||||
const cursorHookModule = require('../hooks/msd-cursor-subagent-start.js');
|
||||
|
||||
let cursorConfigDir;
|
||||
let managedRootPath;
|
||||
@@ -432,7 +432,7 @@ describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof cover
|
||||
let savedCursorConfigDir;
|
||||
|
||||
before(() => {
|
||||
cursorConfigDir = createTempDir('gsd-cs-seam-cursorhome-');
|
||||
cursorConfigDir = createTempDir('msd-cs-seam-cursorhome-');
|
||||
managedRootPath = path.join(cursorConfigDir, 'worktrees');
|
||||
spoofedPath = path.join(managedRootPath, 'spoofed');
|
||||
fs.mkdirSync(spoofedPath, { recursive: true });
|
||||
@@ -464,7 +464,7 @@ describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof cover
|
||||
}
|
||||
|
||||
test('resolveIsolationEvidence: injected realpath resolving the spoofed path OUTSIDE the managed root -> NOT isolated, not merely "cannot determine"', () => {
|
||||
const primaryCheckoutTarget = path.join(require('node:os').tmpdir(), 'gsd-cs-seam-primary-checkout-fake-1');
|
||||
const primaryCheckoutTarget = path.join(require('node:os').tmpdir(), 'msd-cs-seam-primary-checkout-fake-1');
|
||||
const evidence = cursorHookModule.resolveIsolationEvidence(spoofedPath, { realpath: fakeRealpathTo(primaryCheckoutTarget) });
|
||||
assert.equal(evidence.isolated, false, 'realpath must be consulted, not the literal lexically-nested path');
|
||||
assert.equal(evidence.cannotDetermine, false, 'the spoofed path is a real (decoy) git repo — git can determine it fine');
|
||||
@@ -472,9 +472,9 @@ describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof cover
|
||||
});
|
||||
|
||||
test('evaluateRootIsolation: full pipeline denies through the injected realpath seam (no subprocess, no symlink)', () => {
|
||||
const primaryCheckoutTarget = path.join(require('node:os').tmpdir(), 'gsd-cs-seam-primary-checkout-fake-2');
|
||||
const primaryCheckoutTarget = path.join(require('node:os').tmpdir(), 'msd-cs-seam-primary-checkout-fake-2');
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(
|
||||
spoofedPath, 'gsd-executor', { realpath: fakeRealpathTo(primaryCheckoutTarget) },
|
||||
spoofedPath, 'msd-executor', { realpath: fakeRealpathTo(primaryCheckoutTarget) },
|
||||
);
|
||||
assert.equal(verdict.action, 'deny');
|
||||
assert.match(verdict.reason, /not an isolated Cursor worktree/);
|
||||
@@ -490,27 +490,27 @@ describe('gsd-cursor-subagent-start.js: realpath seam — in-process spoof cover
|
||||
// injected realpath mapping simulating a symlink pointing outside the
|
||||
// managed root, not by some incidental property of this fixture.
|
||||
const identityRealpath = (p) => p;
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(spoofedPath, 'gsd-executor', { realpath: identityRealpath });
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(spoofedPath, 'msd-executor', { realpath: identityRealpath });
|
||||
assert.equal(verdict.action, 'allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: nonexistent workspace root does not crash or bypass the scan (#3045)', () => {
|
||||
describe('msd-cursor-subagent-start.js: nonexistent workspace root does not crash or bypass the scan (#3045)', () => {
|
||||
let unisolatedProject;
|
||||
let nonexistentRoot;
|
||||
|
||||
before(() => {
|
||||
// #3045 MAJOR fix: explicit runtime signal required — see the note on
|
||||
// the first `harnessProject` fixture above.
|
||||
unisolatedProject = makeGitProject('gsd-cs-nonexistent-companion-', JSON.stringify({ runtime: 'cursor' }));
|
||||
nonexistentRoot = path.join(require('node:os').tmpdir(), 'gsd-cs-does-not-exist-', String(process.pid), 'nope');
|
||||
unisolatedProject = makeGitProject('msd-cs-nonexistent-companion-', JSON.stringify({ runtime: 'cursor' }));
|
||||
nonexistentRoot = path.join(require('node:os').tmpdir(), 'msd-cs-does-not-exist-', String(process.pid), 'nope');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanup(unisolatedProject);
|
||||
});
|
||||
|
||||
test('single nonexistent workspace root, executor, no other root -> allow (no GSD project confirmable there; the project-existence gate — not the realpath isolation check — is what makes this allow, and is intentionally NOT a fail-closed trigger, mirroring the existing "no workspace root at all" branch)', () => {
|
||||
test('single nonexistent workspace root, executor, no other root -> allow (no MSD project confirmable there; the project-existence gate — not the realpath isolation check — is what makes this allow, and is intentionally NOT a fail-closed trigger, mirroring the existing "no workspace root at all" branch)', () => {
|
||||
const r = runHook(subagentPayload([nonexistentRoot]));
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
assert.equal(r.stderr, '', 'must not crash or log a stack trace on an unresolvable root');
|
||||
@@ -528,14 +528,14 @@ describe('gsd-cursor-subagent-start.js: nonexistent workspace root does not cras
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: output contract precision (#3045)', () => {
|
||||
describe('msd-cursor-subagent-start.js: output contract precision (#3045)', () => {
|
||||
let harnessProject;
|
||||
|
||||
before(() => {
|
||||
// See the #3045 MAJOR fix note in the first `harnessProject` fixture
|
||||
// above — an explicit runtime signal is required for the fallback to
|
||||
// resolve harness-worktree.
|
||||
harnessProject = makeGitProject('gsd-cs-contract-', JSON.stringify({ runtime: 'cursor' }));
|
||||
harnessProject = makeGitProject('msd-cs-contract-', JSON.stringify({ runtime: 'cursor' }));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -558,9 +558,9 @@ describe('gsd-cursor-subagent-start.js: output contract precision (#3045)', () =
|
||||
});
|
||||
|
||||
// ─── Generative Fix Divergence guard (CLAUDE.md) ─────────────────────────────
|
||||
// EXECUTOR_SUBAGENT_TYPES is duplicated between hooks/gsd-agent-isolation-guard.js
|
||||
// (Claude) and hooks/gsd-cursor-subagent-start.js (Cursor) — the same "which
|
||||
// subagent_type strings identify GSD's executor" constant, defined
|
||||
// EXECUTOR_SUBAGENT_TYPES is duplicated between hooks/msd-agent-isolation-guard.js
|
||||
// (Claude) and hooks/msd-cursor-subagent-start.js (Cursor) — the same "which
|
||||
// subagent_type strings identify MSD's executor" constant, defined
|
||||
// independently in two parallel-surface hooks. Per CLAUDE.md's
|
||||
// "Generative Fix Divergence" rule, a shared-concept constant like this needs
|
||||
// a parity assertion that fails if the two definitions drift (e.g. a future
|
||||
@@ -571,10 +571,10 @@ describe('gsd-cursor-subagent-start.js: output contract precision (#3045)', () =
|
||||
// require()-based constant import (both hook files are top-level scripts
|
||||
// with unconditional stdin listeners; requiring them as modules would run
|
||||
// that side-effecting code).
|
||||
describe('executor-identity parity: hooks/gsd-agent-isolation-guard.js (Claude) vs hooks/gsd-cursor-subagent-start.js (Cursor) (#3045)', () => {
|
||||
const CLAUDE_HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-agent-isolation-guard.js');
|
||||
let claudeProject; // harness-worktree GSD project, no isolation param on the call
|
||||
let cursorProject; // harness-worktree GSD project, not an isolated worktree
|
||||
describe('executor-identity parity: hooks/msd-agent-isolation-guard.js (Claude) vs hooks/msd-cursor-subagent-start.js (Cursor) (#3045)', () => {
|
||||
const CLAUDE_HOOK_PATH = path.join(__dirname, '..', 'hooks', 'msd-agent-isolation-guard.js');
|
||||
let claudeProject; // harness-worktree MSD project, no isolation param on the call
|
||||
let cursorProject; // harness-worktree MSD project, not an isolated worktree
|
||||
|
||||
before(() => {
|
||||
// Must be a REAL git repo (not a bare mkdir'd directory) — same reason as
|
||||
@@ -583,7 +583,7 @@ describe('executor-identity parity: hooks/gsd-agent-isolation-guard.js (Claude)
|
||||
// fixture would flip the Claude side to allow and the parity probe would
|
||||
// compare Claude-allowing against Cursor-denying instead of comparing two
|
||||
// ACTUALLY enforcing configurations.
|
||||
claudeProject = makeGitProject('gsd-cs-parity-claude-', JSON.stringify({ runtime: 'claude' }));
|
||||
claudeProject = makeGitProject('msd-cs-parity-claude-', JSON.stringify({ runtime: 'claude' }));
|
||||
|
||||
// Must be a REAL git repo (not a bare mkdir'd directory): the Cursor hook's
|
||||
// #3045 MAJOR 3 "not a git repo -> INERT" branch would otherwise short-circuit
|
||||
@@ -593,7 +593,7 @@ describe('executor-identity parity: hooks/gsd-agent-isolation-guard.js (Claude)
|
||||
// resolve harness-worktree — mirrors claudeProject's explicit
|
||||
// `runtime: 'claude'` above, so this parity check compares two ACTUALLY
|
||||
// enforcing configurations, not Claude-enforcing vs. Cursor-inert.
|
||||
cursorProject = makeGitProject('gsd-cs-parity-cursor-', JSON.stringify({ runtime: 'cursor' }));
|
||||
cursorProject = makeGitProject('msd-cs-parity-cursor-', JSON.stringify({ runtime: 'cursor' }));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -601,12 +601,12 @@ describe('executor-identity parity: hooks/gsd-agent-isolation-guard.js (Claude)
|
||||
cleanup(cursorProject);
|
||||
});
|
||||
|
||||
const PROBE_TYPES = ['gsd-executor', 'gsd-code-reviewer', 'gsd-planner', 'generalPurpose', 'explore', 'shell', 'GSD-EXECUTOR'];
|
||||
const PROBE_TYPES = ['msd-executor', 'msd-code-reviewer', 'msd-planner', 'generalPurpose', 'explore', 'shell', 'MSD-EXECUTOR'];
|
||||
|
||||
for (const subagentType of PROBE_TYPES) {
|
||||
test(`subagent_type="${subagentType}": Claude block-decision and Cursor deny-decision agree`, () => {
|
||||
const claudeEnv = { ...process.env };
|
||||
delete claudeEnv.GSD_RUNTIME;
|
||||
delete claudeEnv.MSD_RUNTIME;
|
||||
const claudeResult = runNode([CLAUDE_HOOK_PATH], {
|
||||
input: JSON.stringify({
|
||||
hook_event_name: 'PreToolUse',
|
||||
@@ -626,19 +626,19 @@ describe('executor-identity parity: hooks/gsd-agent-isolation-guard.js (Claude)
|
||||
assert.equal(
|
||||
cursorBlocked, claudeBlocked,
|
||||
`Cursor and Claude isolation guards disagree on whether subagent_type="${subagentType}" is ` +
|
||||
`a GSD executor (Claude blocked=${claudeBlocked}, Cursor blocked=${cursorBlocked}). ` +
|
||||
`EXECUTOR_SUBAGENT_TYPES has drifted between hooks/gsd-agent-isolation-guard.js and ` +
|
||||
`hooks/gsd-cursor-subagent-start.js.`
|
||||
`a MSD executor (Claude blocked=${claudeBlocked}, Cursor blocked=${cursorBlocked}). ` +
|
||||
`EXECUTOR_SUBAGENT_TYPES has drifted between hooks/msd-agent-isolation-guard.js and ` +
|
||||
`hooks/msd-cursor-subagent-start.js.`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 MAJOR 3 — non-git GSD project is INERT, not a confident negative', () => {
|
||||
describe('msd-cursor-subagent-start.js: #3045 MAJOR 3 — non-git MSD project is INERT, not a confident negative', () => {
|
||||
let nonGitProject; // .planning/config.json present, but NOT a git repo at all, NOT under managed root
|
||||
|
||||
before(() => {
|
||||
nonGitProject = createTempDir('gsd-cs-notgitrepo-');
|
||||
nonGitProject = createTempDir('msd-cs-notgitrepo-');
|
||||
fs.mkdirSync(path.join(nonGitProject, '.planning'), { recursive: true });
|
||||
// #3045 MAJOR fix: explicit runtime signal, so this test exercises the
|
||||
// not_git_repo -> INERT code path specifically, not the unrelated "no
|
||||
@@ -659,11 +659,11 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR 3 — non-git GSD project is
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 MINOR — relative workspace_roots entry does not fail open', () => {
|
||||
describe('msd-cursor-subagent-start.js: #3045 MINOR — relative workspace_roots entry does not fail open', () => {
|
||||
test('a relative-path workspace_roots entry is filtered out, not resolved against the hook cwd', () => {
|
||||
// Before the fix, a relative entry joined against process.cwd() (Cursor's
|
||||
// config dir, ~/.cursor for a real invocation) — almost certainly NOT a
|
||||
// GSD project — which read as "not a GSD project" and allowed. The fix
|
||||
// MSD project — which read as "not a MSD project" and allowed. The fix
|
||||
// filters relative entries out of getWorkspaceRoots() instead, for the
|
||||
// honest reason (never a resolvable workspace root), but the OBSERVABLE
|
||||
// outcome for this single-relative-root case is still allow either way,
|
||||
@@ -680,7 +680,7 @@ describe('gsd-cursor-subagent-start.js: #3045 MINOR — relative workspace_roots
|
||||
test('relative root paired with a real unisolated absolute harness-worktree root still DENIES (relative entry is dropped, not silently trusted)', () => {
|
||||
// #3045 MAJOR fix: explicit runtime signal required — see the note on
|
||||
// the first `harnessProject` fixture above.
|
||||
const unisolatedProject = makeGitProject('gsd-cs-relmix-', JSON.stringify({ runtime: 'cursor' }));
|
||||
const unisolatedProject = makeGitProject('msd-cs-relmix-', JSON.stringify({ runtime: 'cursor' }));
|
||||
try {
|
||||
const r = runHook(subagentPayload(['relative/workspace/root', unisolatedProject]));
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
@@ -692,8 +692,8 @@ describe('gsd-cursor-subagent-start.js: #3045 MINOR — relative workspace_roots
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is authoritative over registry capability', () => {
|
||||
// Mirrors tests/gsd-agent-isolation-guard.test.cjs's Claude-side pinning
|
||||
describe('msd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is authoritative over registry capability', () => {
|
||||
// Mirrors tests/msd-agent-isolation-guard.test.cjs's Claude-side pinning
|
||||
// for the same defect: the guard used to key enforcement on the REGISTRY's
|
||||
// dispatch.isolation (a host CAPABILITY), not the workflow's resolved
|
||||
// per-dispatch ISOLATION. `harnessProject` resolves to harness-worktree via
|
||||
@@ -708,8 +708,8 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
// resolve harness-worktree (both fixtures need it: `useWorktreesFalseProject`
|
||||
// must actually reach the `workflow.use_worktrees:false` branch, not
|
||||
// short-circuit to 'none' for the unrelated "no runtime signal" reason).
|
||||
harnessProject = makeGitProject('gsd-cs-sentinel-', JSON.stringify({ runtime: 'cursor' }));
|
||||
useWorktreesFalseProject = makeGitProject('gsd-cs-uwf-', JSON.stringify({ runtime: 'cursor', workflow: { use_worktrees: false } }));
|
||||
harnessProject = makeGitProject('msd-cs-sentinel-', JSON.stringify({ runtime: 'cursor' }));
|
||||
useWorktreesFalseProject = makeGitProject('msd-cs-uwf-', JSON.stringify({ runtime: 'cursor', workflow: { use_worktrees: false } }));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -725,7 +725,7 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, undefined, `expected allow, got: ${r.stdout}`);
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -737,7 +737,7 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, undefined);
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -749,7 +749,7 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, 'deny', `expected deny (stale sentinel ignored), got: ${r.stdout}`);
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -763,7 +763,7 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, 'deny');
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -782,11 +782,11 @@ describe('gsd-cursor-subagent-start.js: #3045 BLOCKER regression — sentinel is
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 MAJOR "Cursor residual false-deny" — align with Claude hook semantics', () => {
|
||||
let unconfiguredProject; // .planning/config.json = {}, no GSD_RUNTIME, no defaults.json signal
|
||||
describe('msd-cursor-subagent-start.js: #3045 MAJOR "Cursor residual false-deny" — align with Claude hook semantics', () => {
|
||||
let unconfiguredProject; // .planning/config.json = {}, no MSD_RUNTIME, no defaults.json signal
|
||||
|
||||
before(() => {
|
||||
unconfiguredProject = makeGitProject('gsd-cs-unconfigured-', JSON.stringify({}));
|
||||
unconfiguredProject = makeGitProject('msd-cs-unconfigured-', JSON.stringify({}));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -794,25 +794,25 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR "Cursor residual false-deny"
|
||||
});
|
||||
|
||||
test('no sentinel + no runtime signal anywhere -> ALLOW (was a confident "cursor" default -> DENY pre-fix)', () => {
|
||||
// Before this fix, this exact shape (a GSD project scaffolded from
|
||||
// gsd-core/templates/config.json, which ships with no `runtime` key, with
|
||||
// no fresh sentinel — outside execute-phase, after .gsd cleanup, or past
|
||||
// Before this fix, this exact shape (a MSD project scaffolded from
|
||||
// msd-core/templates/config.json, which ships with no `runtime` key, with
|
||||
// no fresh sentinel — outside execute-phase, after .msd cleanup, or past
|
||||
// the sentinel's staleness window) always resolved 'cursor' purely
|
||||
// because this script only runs as Cursor's own hook, then hard-DENIED
|
||||
// because the main checkout is not (yet) an isolated Cursor worktree —
|
||||
// a false-deny of an otherwise legitimate dispatch. HOME is pinned to the
|
||||
// project dir itself (which has no .gsd/defaults.json) for hermeticity.
|
||||
// project dir itself (which has no .msd/defaults.json) for hermeticity.
|
||||
const r = runHook(subagentPayload([unconfiguredProject]), { HOME: unconfiguredProject });
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.permission, undefined, `expected allow (inert), got: ${r.stdout}`);
|
||||
});
|
||||
|
||||
test('~/.gsd/defaults.json runtime (installer-persisted, #2395) makes a REAL Cursor+GSD install still enforce', () => {
|
||||
const home = createTempDir('gsd-cs-defaults-home-');
|
||||
test('~/.msd/defaults.json runtime (installer-persisted, #2395) makes a REAL Cursor+MSD install still enforce', () => {
|
||||
const home = createTempDir('msd-cs-defaults-home-');
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: 'cursor' }));
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
fs.writeFileSync(path.join(home, '.msd', 'defaults.json'), JSON.stringify({ runtime: 'cursor' }));
|
||||
|
||||
const r = runHook(subagentPayload([unconfiguredProject]), { HOME: home });
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
@@ -824,11 +824,11 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR "Cursor residual false-deny"
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 SECURITY F2 — sentinel bound to phase/plan, mismatch is "no applicable sentinel"', () => {
|
||||
describe('msd-cursor-subagent-start.js: #3045 SECURITY F2 — sentinel bound to phase/plan, mismatch is "no applicable sentinel"', () => {
|
||||
let harnessProject;
|
||||
|
||||
before(() => {
|
||||
harnessProject = makeGitProject('gsd-cs-f2-', JSON.stringify({ runtime: 'cursor' }));
|
||||
harnessProject = makeGitProject('msd-cs-f2-', JSON.stringify({ runtime: 'cursor' }));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -842,7 +842,7 @@ describe('gsd-cursor-subagent-start.js: #3045 SECURITY F2 — sentinel bound to
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
assert.equal(JSON.parse(r.stdout).permission, 'deny', 'mismatched sentinel must not silently allow');
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -853,16 +853,16 @@ describe('gsd-cursor-subagent-start.js: #3045 SECURITY F2 — sentinel bound to
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
assert.equal(JSON.parse(r.stdout).permission, undefined);
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #4594 row 30 — sentinel-discard reporting parity with the Claude guard', () => {
|
||||
describe('msd-cursor-subagent-start.js: #4594 row 30 — sentinel-discard reporting parity with the Claude guard', () => {
|
||||
let harnessProject;
|
||||
|
||||
before(() => {
|
||||
harnessProject = makeGitProject('gsd-cs-4594-', JSON.stringify({ runtime: 'cursor' }));
|
||||
harnessProject = makeGitProject('msd-cs-4594-', JSON.stringify({ runtime: 'cursor' }));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -875,7 +875,7 @@ describe('gsd-cursor-subagent-start.js: #4594 row 30 — sentinel-discard report
|
||||
// plan (see hooks/lib/dispatch-identity.js's findProse doc comment), so
|
||||
// only phase is compared here — exactly the real-world match path.
|
||||
writeSentinel(harnessProject, { isolation: 'none', phase: '03', plan: '03-02-hardening' });
|
||||
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
|
||||
t.after(() => cleanup(path.join(harnessProject, '.msd')));
|
||||
const r = runHook(subagentPayload([harnessProject], { task: 'Execute plan 02 of phase 03-auth.' }));
|
||||
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
|
||||
const out = JSON.parse(r.stdout);
|
||||
@@ -883,22 +883,22 @@ describe('gsd-cursor-subagent-start.js: #4594 row 30 — sentinel-discard report
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3045 MAJOR — clock seam boundary coverage (in-process, no subprocess wall-clock race)', () => {
|
||||
const cursorHookModule = require('../hooks/gsd-cursor-subagent-start.js');
|
||||
describe('msd-cursor-subagent-start.js: #3045 MAJOR — clock seam boundary coverage (in-process, no subprocess wall-clock race)', () => {
|
||||
const cursorHookModule = require('../hooks/msd-cursor-subagent-start.js');
|
||||
|
||||
let harnessProject;
|
||||
let savedGsdRuntime;
|
||||
let savedMsdRuntime;
|
||||
|
||||
before(() => {
|
||||
harnessProject = makeGitProject('gsd-cs-clock-', JSON.stringify({ runtime: 'cursor' }));
|
||||
savedGsdRuntime = process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
harnessProject = makeGitProject('msd-cs-clock-', JSON.stringify({ runtime: 'cursor' }));
|
||||
savedMsdRuntime = process.env.MSD_RUNTIME;
|
||||
delete process.env.MSD_RUNTIME;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanup(harnessProject);
|
||||
if (savedGsdRuntime === undefined) delete process.env.GSD_RUNTIME;
|
||||
else process.env.GSD_RUNTIME = savedGsdRuntime;
|
||||
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
|
||||
else process.env.MSD_RUNTIME = savedMsdRuntime;
|
||||
});
|
||||
|
||||
function fixedClock(nowMs) {
|
||||
@@ -910,11 +910,11 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR — clock seam boundary cove
|
||||
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
|
||||
try {
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(
|
||||
harnessProject, 'gsd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS - 1) },
|
||||
harnessProject, 'msd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS - 1) },
|
||||
);
|
||||
assert.equal(verdict.action, 'allow', 'still within the trust window — must use the fresh "none" sentinel');
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -923,11 +923,11 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR — clock seam boundary cove
|
||||
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
|
||||
try {
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(
|
||||
harnessProject, 'gsd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS) },
|
||||
harnessProject, 'msd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS) },
|
||||
);
|
||||
assert.equal(verdict.action, 'deny');
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -936,32 +936,32 @@ describe('gsd-cursor-subagent-start.js: #3045 MAJOR — clock seam boundary cove
|
||||
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
|
||||
try {
|
||||
const verdict = cursorHookModule.evaluateRootIsolation(
|
||||
harnessProject, 'gsd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS + 1) },
|
||||
harnessProject, 'msd-executor', { clock: fixedClock(writtenAt + SENTINEL_STALE_MS + 1) },
|
||||
);
|
||||
assert.equal(verdict.action, 'deny');
|
||||
} finally {
|
||||
cleanup(path.join(harnessProject, '.gsd'));
|
||||
cleanup(path.join(harnessProject, '.msd'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-cursor-subagent-start.js: #3566 — per-install .gsd-runtime marker rung (in-process)', () => {
|
||||
describe('msd-cursor-subagent-start.js: #3566 — per-install .msd-runtime marker rung (in-process)', () => {
|
||||
// Same seam contract as the agent guard's #3566 block in
|
||||
// tests/gsd-agent-isolation-guard.test.cjs: the marker is __dirname-relative
|
||||
// tests/msd-agent-isolation-guard.test.cjs: the marker is __dirname-relative
|
||||
// in production, so a spawned hook in this dev tree (no marker) can never
|
||||
// exercise the rung — require the module and drive the seam directly.
|
||||
const cursorHookModule = require('../hooks/gsd-cursor-subagent-start.js');
|
||||
const cursorHookModule = require('../hooks/msd-cursor-subagent-start.js');
|
||||
|
||||
let savedHome;
|
||||
let savedUserProfile;
|
||||
let savedGsdRuntime;
|
||||
let savedMsdRuntime;
|
||||
let project; // scaffold-shaped config ({}), per #2840's no-runtime-key template
|
||||
|
||||
before(() => {
|
||||
savedHome = process.env.HOME;
|
||||
savedUserProfile = process.env.USERPROFILE;
|
||||
savedGsdRuntime = process.env.GSD_RUNTIME;
|
||||
project = createTempDir('gsd-cs-3566-');
|
||||
savedMsdRuntime = process.env.MSD_RUNTIME;
|
||||
project = createTempDir('msd-cs-3566-');
|
||||
fs.mkdirSync(path.join(project, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(path.join(project, '.planning', 'config.json'), JSON.stringify({}));
|
||||
});
|
||||
@@ -972,17 +972,17 @@ describe('gsd-cursor-subagent-start.js: #3566 — per-install .gsd-runtime marke
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
if (savedGsdRuntime === undefined) delete process.env.GSD_RUNTIME;
|
||||
else process.env.GSD_RUNTIME = savedGsdRuntime;
|
||||
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
|
||||
else process.env.MSD_RUNTIME = savedMsdRuntime;
|
||||
cursorHookModule._setInstallRuntimeMarkerForTests(null);
|
||||
});
|
||||
|
||||
// Redirects HOME (mirrored onto USERPROFILE for Windows) at a fake home with
|
||||
// an optional defaults.json naming `defaultsRuntime`.
|
||||
function pinHome(t, defaultsRuntime) {
|
||||
const home = createTempDir('gsd-cs-3566-home-');
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: defaultsRuntime }));
|
||||
const home = createTempDir('msd-cs-3566-home-');
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
fs.writeFileSync(path.join(home, '.msd', 'defaults.json'), JSON.stringify({ runtime: defaultsRuntime }));
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
t.after(() => cleanup(home));
|
||||
@@ -998,7 +998,7 @@ describe('gsd-cursor-subagent-start.js: #3566 — per-install .gsd-runtime marke
|
||||
// (here: orchestrator-worktree — not harness-worktree); post-fix it must
|
||||
// resolve claude → harness-worktree.
|
||||
pinHome(t, 'codex');
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.MSD_RUNTIME;
|
||||
cursorHookModule._setInstallRuntimeMarkerForTests('claude');
|
||||
t.after(() => cursorHookModule._setInstallRuntimeMarkerForTests(null));
|
||||
assert.equal(fallback(), 'harness-worktree');
|
||||
@@ -1006,18 +1006,18 @@ describe('gsd-cursor-subagent-start.js: #3566 — per-install .gsd-runtime marke
|
||||
|
||||
test('#3566 (negative control): absent marker still falls through to the defaults rung', (t) => {
|
||||
pinHome(t, 'codex');
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.MSD_RUNTIME;
|
||||
cursorHookModule._setInstallRuntimeMarkerForTests(null);
|
||||
assert.equal(fallback(), 'orchestrator-worktree', 'defaults.json remains the final rung (#3045 behavior intact)');
|
||||
});
|
||||
|
||||
test('#3566 (negative control): explicit config.json runtime still outranks the marker', (t) => {
|
||||
const cfgProject = createTempDir('gsd-cs-3566-cfg-');
|
||||
const cfgProject = createTempDir('msd-cs-3566-cfg-');
|
||||
fs.mkdirSync(path.join(cfgProject, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(path.join(cfgProject, '.planning', 'config.json'), JSON.stringify({ runtime: 'codex' }));
|
||||
t.after(() => cleanup(cfgProject));
|
||||
pinHome(t, 'codex');
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.MSD_RUNTIME;
|
||||
cursorHookModule._setInstallRuntimeMarkerForTests('claude');
|
||||
t.after(() => cursorHookModule._setInstallRuntimeMarkerForTests(null));
|
||||
assert.equal(
|
||||
@@ -1028,33 +1028,33 @@ describe('gsd-cursor-subagent-start.js: #3566 — per-install .gsd-runtime marke
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3582: cold tree (no gsd-core/bin/lib/*.cjs) — self-heal surfacing ────
|
||||
// ─── #3582: cold tree (no msd-core/bin/lib/*.cjs) — self-heal surfacing ────
|
||||
//
|
||||
// Mirrors tests/gsd-agent-isolation-guard.test.cjs's identical #3582 case for
|
||||
// Mirrors tests/msd-agent-isolation-guard.test.cjs's identical #3582 case for
|
||||
// the sibling Claude guard. evaluateRootIsolation now calls
|
||||
// ensureRuntimeBuild() immediately after the GSD-project existence check —
|
||||
// ensureRuntimeBuild() immediately after the MSD-project existence check —
|
||||
// before resolveFallbackIsolation's runtime-name-policy.cjs/
|
||||
// capability-registry.cjs requires or resolveIsolationEvidence's
|
||||
// runtime-homes.cjs/worktree-safety.cjs requires — so a missing compiled
|
||||
// runtime library denies with the seam's own actionable message rather than
|
||||
// the generic "could not read or resolve ... configuration" text (#3050).
|
||||
// Simulated hermetically via tests/helpers/cold-runtime-lib-fixture.cjs — the
|
||||
// REAL gsd-core/bin/lib/ is never touched.
|
||||
describe('gsd-cursor-subagent-start.js: #3582 cold tree — RuntimeBuildError surfaces distinctly', () => {
|
||||
// REAL msd-core/bin/lib/ is never touched.
|
||||
describe('msd-cursor-subagent-start.js: #3582 cold tree — RuntimeBuildError surfaces distinctly', () => {
|
||||
const { buildColdInstallTree } = require('./helpers/cold-runtime-lib-fixture.cjs');
|
||||
|
||||
test('missing compiled runtime library -> DENY (fail-closed) with the seam\'s own actionable message, not the generic config-unreadable text', (t) => {
|
||||
const cold = buildColdInstallTree();
|
||||
t.after(cold.cleanup);
|
||||
const project = createTempDir('gsd-cs-cold-');
|
||||
const project = createTempDir('msd-cs-cold-');
|
||||
t.after(() => cleanup(project));
|
||||
fs.mkdirSync(path.join(project, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(path.join(project, '.planning', 'config.json'), JSON.stringify({ runtime: 'cursor' }));
|
||||
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_RUNTIME;
|
||||
delete env.MSD_RUNTIME;
|
||||
delete env.CURSOR_CONFIG_DIR;
|
||||
const r = toLegacyResult(runNode([path.join(cold.hooksDir, 'gsd-cursor-subagent-start.js')], {
|
||||
const r = toLegacyResult(runNode([path.join(cold.hooksDir, 'msd-cursor-subagent-start.js')], {
|
||||
input: JSON.stringify(subagentPayload([project])),
|
||||
cwd: require('node:os').tmpdir(),
|
||||
env,
|
||||
|
||||
Reference in New Issue
Block a user