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:
@@ -12,7 +12,7 @@
|
||||
* F. No origin/HEAD, no remote show, no local branches → returns "main" (last resort)
|
||||
* G. Anti-regression guard: five affected workflows must NOT contain the
|
||||
* duplicated bare `:-main` / `:-master` fallback pattern that was the root cause.
|
||||
* They must call `gsd_run query git.base-branch` instead.
|
||||
* They must call `msd_run query git.base-branch` instead.
|
||||
* (see the source-text-is-the-product exemption declared below this docblock —
|
||||
* the workflow .md content IS the runtime surface; the absence of the bad
|
||||
* pattern is what ships to agents.)
|
||||
@@ -29,8 +29,8 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { runGsdTools, cleanup, readFileNormalized } = require('./helpers.cjs');
|
||||
const { ExitError } = require('../gsd-core/bin/lib/cli-exit.cjs');
|
||||
const { runMsdTools, cleanup, readFileNormalized } = require('./helpers.cjs');
|
||||
const { ExitError } = require('../msd-core/bin/lib/cli-exit.cjs');
|
||||
const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
|
||||
const { gitOrThrow, throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
@@ -40,7 +40,7 @@ const { GIT_TIMEOUT_MS, HOOK_FANOUT_TIMEOUT_MS } = require('./helpers/timeouts.c
|
||||
|
||||
/**
|
||||
* The #3819 executor pre-commit protected-branch guard test harness below
|
||||
* invokes a bash script with fake `git`/`gsd_run` shell functions defined
|
||||
* invokes a bash script with fake `git`/`msd_run` shell functions defined
|
||||
* INLINE — no real subprocess fan-out happens, so this is lighter than
|
||||
* `HOOK_FANOUT_TIMEOUT_MS` (60000ms, sized for a hook that fans out to
|
||||
* nested shell spawns). Pre-existing value, unchanged by this migration.
|
||||
@@ -72,7 +72,7 @@ const WORKTREE_INFO_TIMEOUT_MS = 5000;
|
||||
* and local branches.
|
||||
*/
|
||||
function createGitRepo(opts = {}) {
|
||||
const { prefix = 'gsd-1146-', defaultBranch = 'master' } = opts;
|
||||
const { prefix = 'msd-1146-', defaultBranch = 'master' } = opts;
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
gitOrThrow(['init', '-b', defaultBranch], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
|
||||
gitOrThrow(['config', 'user.email', 'test@test.com'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
|
||||
@@ -86,7 +86,7 @@ function createGitRepo(opts = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a .planning dir so gsd-tools resolveProjectRoot doesn't bail.
|
||||
* Create a .planning dir so msd-tools resolveProjectRoot doesn't bail.
|
||||
*/
|
||||
function addPlanning(dir) {
|
||||
fs.mkdirSync(path.join(dir, '.planning', 'phases'), { recursive: true });
|
||||
@@ -108,9 +108,9 @@ function snapshotPlanningTree(dir) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a gsd config.json with git.base_branch set.
|
||||
* Write a msd config.json with git.base_branch set.
|
||||
*/
|
||||
function setGsdConfig(dir, key, value) {
|
||||
function setMsdConfig(dir, key, value) {
|
||||
const cfgDir = path.join(dir, '.planning');
|
||||
fs.mkdirSync(cfgDir, { recursive: true });
|
||||
const cfgPath = path.join(cfgDir, 'config.json');
|
||||
@@ -125,21 +125,21 @@ function setGsdConfig(dir, key, value) {
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const k = parts[i];
|
||||
if (k === '__proto__' || k === 'prototype' || k === 'constructor') {
|
||||
throw new Error(`setGsdConfig: unsafe config key segment '${k}'`);
|
||||
throw new Error(`setMsdConfig: unsafe config key segment '${k}'`);
|
||||
}
|
||||
if (typeof obj[k] !== 'object' || obj[k] === null) obj[k] = {};
|
||||
obj = obj[k];
|
||||
}
|
||||
const lastKey = parts[parts.length - 1];
|
||||
if (lastKey === '__proto__' || lastKey === 'prototype' || lastKey === 'constructor') {
|
||||
throw new Error(`setGsdConfig: unsafe config key segment '${lastKey}'`);
|
||||
throw new Error(`setMsdConfig: unsafe config key segment '${lastKey}'`);
|
||||
}
|
||||
obj[lastKey] = value;
|
||||
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
||||
}
|
||||
|
||||
// Paths to the five affected workflow files
|
||||
const WORKFLOW_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
const WORKFLOW_DIR = path.join(__dirname, '..', 'msd-core', 'workflows');
|
||||
const AFFECTED_WORKFLOWS = [
|
||||
path.join(WORKFLOW_DIR, 'execute-phase.md'),
|
||||
path.join(WORKFLOW_DIR, 'quick.md'),
|
||||
@@ -153,12 +153,12 @@ const AFFECTED_WORKFLOWS = [
|
||||
describe('#1146: git.base-branch resolver', () => {
|
||||
|
||||
test('A. config override git.base_branch → returned immediately', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-a-', defaultBranch: 'master' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-a-', defaultBranch: 'master' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
setGsdConfig(dir, 'git.base_branch', 'develop');
|
||||
setMsdConfig(dir, 'git.base_branch', 'develop');
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch with config override failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'develop',
|
||||
@@ -167,8 +167,8 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
|
||||
test('B. origin/HEAD symref resolves → returned', (t) => {
|
||||
// Create an "origin" bare repo with main branch
|
||||
const originDir = createGitRepo({ prefix: 'gsd-1146-b-origin-', defaultBranch: 'main' });
|
||||
const worktreeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1146-b-wt-'));
|
||||
const originDir = createGitRepo({ prefix: 'msd-1146-b-origin-', defaultBranch: 'main' });
|
||||
const worktreeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1146-b-wt-'));
|
||||
t.after(() => { cleanup(originDir); cleanup(worktreeDir); });
|
||||
|
||||
// Clone from origin — this sets origin/HEAD
|
||||
@@ -181,7 +181,7 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
const symref = gitOrThrow(['symbolic-ref', 'refs/remotes/origin/HEAD'], { cwd: worktreeDir, timeoutMs: GIT_TIMEOUT_MS }).trim();
|
||||
assert.ok(symref.includes('origin/main'), `Expected origin/HEAD→origin/main, got: ${symref}`);
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], worktreeDir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], worktreeDir);
|
||||
assert.ok(result.success, `git.base-branch symref test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'main',
|
||||
@@ -191,8 +191,8 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
test('C. KEY REGRESSION — master repo, origin/HEAD unset → returns "master" not "main"', (t) => {
|
||||
// This is the bug: git init + remote add without git remote set-head → no origin/HEAD
|
||||
// Current code falls back to :-main → wrong. Fixed code uses `git remote show origin`.
|
||||
const originDir = createGitRepo({ prefix: 'gsd-1146-c-origin-', defaultBranch: 'master' });
|
||||
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1146-c-clone-'));
|
||||
const originDir = createGitRepo({ prefix: 'msd-1146-c-origin-', defaultBranch: 'master' });
|
||||
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1146-c-clone-'));
|
||||
t.after(() => { cleanup(originDir); cleanup(cloneDir); });
|
||||
|
||||
// Manually add remote WITHOUT cloning (so origin/HEAD is never set)
|
||||
@@ -217,7 +217,7 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
}
|
||||
assert.strictEqual(hasSymref, false, 'Test setup: origin/HEAD must be unset for this test case');
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], cloneDir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], cloneDir);
|
||||
assert.ok(result.success, `git.base-branch regression test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'master',
|
||||
@@ -225,12 +225,12 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
});
|
||||
|
||||
test('D. No remote, local branch "master" present, "main" absent → returns "master"', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-d-', defaultBranch: 'master' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-d-', defaultBranch: 'master' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
// No remote configured — falls through to local branch detection
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch local branch test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'master',
|
||||
@@ -238,11 +238,11 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
});
|
||||
|
||||
test('E. No remote, local branch "main" present → returns "main"', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-e-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-e-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch main branch test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'main',
|
||||
@@ -250,12 +250,12 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
});
|
||||
|
||||
test('F. No remote, no main/master local branch → returns "main" (last resort default)', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-f-', defaultBranch: 'develop' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-f-', defaultBranch: 'develop' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
// Branch named "develop" — neither main nor master
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch default fallback test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'main',
|
||||
@@ -263,14 +263,14 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
});
|
||||
|
||||
test('A2. config override with flat base_branch key (legacy form) → returned immediately', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-a2-', defaultBranch: 'master' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-a2-', defaultBranch: 'master' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
// Write flat base_branch directly to config root (legacy form, not nested under "git")
|
||||
const cfgPath = require('node:path').join(dir, '.planning', 'config.json');
|
||||
require('node:fs').writeFileSync(cfgPath, JSON.stringify({ base_branch: 'release' }, null, 2) + '\n');
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch with flat config key failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'release',
|
||||
@@ -285,7 +285,7 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
// value back. `normalizeLegacyKeys` must hoist/resolve `base_branch` the
|
||||
// same way it already does `branching_strategy` — canonical nested wins,
|
||||
// stale flat top-level is dropped.
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-a3-', defaultBranch: 'master' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-a3-', defaultBranch: 'master' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
const cfgPath = require('node:path').join(dir, '.planning', 'config.json');
|
||||
@@ -294,7 +294,7 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
JSON.stringify({ base_branch: 'stale-flat', git: { base_branch: 'canonical-nested' } }, null, 2) + '\n',
|
||||
);
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch with both config keys failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'canonical-nested',
|
||||
@@ -304,25 +304,25 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
test('H. No remote, both "main" and "master" local branches exist → returns "main" (main wins tie-break)', (t) => {
|
||||
// Tier-4 tie-break: when both main and master exist locally and no remote info is available,
|
||||
// "main" wins (documented in tryLocalBranch JSDoc — modern default).
|
||||
const dir = createGitRepo({ prefix: 'gsd-1146-h-', defaultBranch: 'master' });
|
||||
const dir = createGitRepo({ prefix: 'msd-1146-h-', defaultBranch: 'master' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
// Create a "main" branch alongside the existing "master"
|
||||
gitOrThrow(['branch', 'main'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
|
||||
// No remote configured — falls to tier-4 (local branch existence)
|
||||
|
||||
const result = runGsdTools(['query', 'git.base-branch'], dir);
|
||||
const result = runMsdTools(['query', 'git.base-branch'], dir);
|
||||
assert.ok(result.success, `git.base-branch both-branches test failed:\n${result.error}`);
|
||||
const branch = result.output.trim();
|
||||
assert.strictEqual(branch, 'main',
|
||||
`Expected 'main' to win when both main and master exist locally, got: '${branch}'`);
|
||||
});
|
||||
|
||||
test('G. Anti-regression: all five affected workflows use gsd_run query git.base-branch, not bare :-main / :-master', () => {
|
||||
test('G. Anti-regression: all five affected workflows use msd_run query git.base-branch, not bare :-main / :-master', () => {
|
||||
// The root-cause pattern: DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} or BASE_BRANCH="${BASE_BRANCH:-main}"
|
||||
// After fix: workflows call gsd_run query git.base-branch and remove the bare fallback.
|
||||
// After fix: workflows call msd_run query git.base-branch and remove the bare fallback.
|
||||
const BAD_PATTERN = /\$\{(?:DEFAULT_BRANCH|BASE_BRANCH):-(?:main|master)\}/;
|
||||
const RESOLVER_CALL = /gsd_run query git\.base-branch/;
|
||||
const RESOLVER_CALL = /msd_run query git\.base-branch/;
|
||||
|
||||
for (const wfPath of AFFECTED_WORKFLOWS) {
|
||||
const name = path.basename(wfPath);
|
||||
@@ -331,12 +331,12 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
assert.ok(
|
||||
!BAD_PATTERN.test(content),
|
||||
`${name} still contains the bare :-main/:-master fallback pattern. ` +
|
||||
'Must be replaced with gsd_run query git.base-branch (Issue #1146).',
|
||||
'Must be replaced with msd_run query git.base-branch (Issue #1146).',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
RESOLVER_CALL.test(content),
|
||||
`${name} does not call \`gsd_run query git.base-branch\`. ` +
|
||||
`${name} does not call \`msd_run query git.base-branch\`. ` +
|
||||
'All five affected workflows must delegate to the single resolver (Issue #1146).',
|
||||
);
|
||||
}
|
||||
@@ -345,12 +345,12 @@ describe('#1146: git.base-branch resolver', () => {
|
||||
|
||||
// ─── gitWorktreeInfoInternal: behaviour (#1268 T0, T1 #1277) ─────────────────
|
||||
|
||||
const gitBaseBranch = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'git-base-branch.cjs'));
|
||||
const gitBaseBranch = require(path.join(__dirname, '..', 'msd-core', 'bin', 'lib', 'git-base-branch.cjs'));
|
||||
const { createTempGitProject, createTempDir } = require('./helpers.cjs');
|
||||
|
||||
describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
|
||||
test('gitWorktreeInfoInternal(createTempGitProject()) returns {inside:true, worktreeRoot:<non-empty string>}', (t) => {
|
||||
const dir = createTempGitProject('gsd-wt-info-');
|
||||
const dir = createTempGitProject('msd-wt-info-');
|
||||
t.after(() => cleanup(dir));
|
||||
const result = gitBaseBranch.gitWorktreeInfoInternal(dir);
|
||||
// `git rev-parse --show-toplevel` reports the resolved (symlink-free) path,
|
||||
@@ -366,7 +366,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
|
||||
// realpath, or the assertion just encodes the separator convention of
|
||||
// whatever platform it was written on. This is separators only — POSIX's
|
||||
// `replace` is a no-op there, so the assertion keeps its full strength on
|
||||
// POSIX. The remote gsd-test matrix is Linux-only and cannot exercise this
|
||||
// POSIX. The remote msd-test matrix is Linux-only and cannot exercise this
|
||||
// path; it only surfaced on the Windows GitHub Actions shard.
|
||||
assert.strictEqual(result.inside, true, 'inside must be true for a git project dir');
|
||||
assert.strictEqual(result.worktreeRoot, fs.realpathSync.native(dir).replace(/\\/g, '/'),
|
||||
@@ -374,7 +374,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
|
||||
});
|
||||
|
||||
test('gitWorktreeInfoInternal(createTempDir()) returns {inside:false, worktreeRoot:null} for a non-git dir', (t) => {
|
||||
const dir = createTempDir('gsd-wt-info-nongit-');
|
||||
const dir = createTempDir('msd-wt-info-nongit-');
|
||||
t.after(() => cleanup(dir));
|
||||
const result = gitBaseBranch.gitWorktreeInfoInternal(dir);
|
||||
assert.strictEqual(result.inside, false, 'inside must be false for a non-git dir');
|
||||
@@ -401,7 +401,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
|
||||
|
||||
describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last-resort default', () => {
|
||||
test('every tier-2/3/4 git query TIMES OUT → last-resort "main" is UNVERIFIED', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-fault-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-fault-'));
|
||||
t.after(() => cleanup(dir));
|
||||
// No .planning/config.json in this dir → the config-override tier is
|
||||
// skipped naturally (loadConfig's real-fs read misses cleanly).
|
||||
@@ -414,7 +414,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
|
||||
});
|
||||
|
||||
test('every tier-2/3/4 git query cleanly reports no candidate → last-resort "main" is VERIFIED', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-clean-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-clean-'));
|
||||
t.after(() => cleanup(dir));
|
||||
// Default passthrough: exitCode 0, empty stdout for every call — a real,
|
||||
// completed "no answer" from git, not a failure (timedOut:false, error:null).
|
||||
@@ -427,7 +427,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
|
||||
});
|
||||
|
||||
test('resolveBaseBranch (string-returning) is unaffected — both cases still return "main"', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-compat-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-compat-'));
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(
|
||||
gitBaseBranch.resolveBaseBranch(dir, { execGit: makeFaultyGit({ faults: [{ kind: 'timeout' }] }) }),
|
||||
@@ -440,7 +440,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
|
||||
});
|
||||
|
||||
test('cmdGitBaseBranch writes an unverified-fallback diagnostic to stderr ONLY when unverified', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-cmd-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-cmd-'));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
let stdoutText = '';
|
||||
@@ -474,7 +474,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
|
||||
// failure) silently answered `false` — the wrong failure direction for a
|
||||
// protection guard. An unverified guess must not let a caller conclude
|
||||
// "definitely not protected".
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3648-major-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3648-major-'));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
let stdoutText = '';
|
||||
@@ -558,15 +558,15 @@ function extractProtectedBranchWarningBash(workflowFile, stepName) {
|
||||
if (refInBash) refBuffer.push(line);
|
||||
}
|
||||
block = refBlocks.find((candidate) => candidate.includes('--is-protected'));
|
||||
// #3648: sync-runtime-launcher.cjs adds the canonical gsd_run resolver
|
||||
// #3648: sync-runtime-launcher.cjs adds the canonical msd_run resolver
|
||||
// preamble to this standalone step file (runtime-launcher-parity's
|
||||
// one-preamble-per-file rule). That preamble defines its own gsd_run(),
|
||||
// one-preamble-per-file rule). That preamble defines its own msd_run(),
|
||||
// which would shadow this harness's injected mock and reach the real
|
||||
// gsd-tools.cjs on the machine running the test. The preamble's own
|
||||
// msd-tools.cjs on the machine running the test. The preamble's own
|
||||
// correctness is covered by tests/runtime-launcher-parity.test.cjs; this
|
||||
// harness only needs the #3552 warning logic, so strip it here.
|
||||
if (block) {
|
||||
block = block.split('\n').filter((l) => !l.trimStart().startsWith('_GSD_SHIM_NAME=')).join('\n');
|
||||
block = block.split('\n').filter((l) => !l.trimStart().startsWith('_MSD_SHIM_NAME=')).join('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,7 +589,7 @@ function writeProtectedBranchWarningScript(prefix, bash) {
|
||||
' fi',
|
||||
' printf "%s\\n" "$CURRENT_BRANCH_VALUE"',
|
||||
'}',
|
||||
'gsd_run() {',
|
||||
'msd_run() {',
|
||||
' if [ "$#" -ne 4 ] || [ "$1" != query ] || [ "$2" != git.base-branch ] ||',
|
||||
' [ "$3" != --is-protected ] || [ "$4" != "$CURRENT_BRANCH_VALUE" ]; then',
|
||||
' printf "unexpected protected-branch query\\n"',
|
||||
@@ -599,7 +599,7 @@ function writeProtectedBranchWarningScript(prefix, bash) {
|
||||
// explanations to stderr. Emitting one here is what makes a swallowed
|
||||
// `2>/dev/null` at the call site visible to a test.
|
||||
' if [ -n "${DIAGNOSTIC_TEXT:-}" ]; then printf "%s\\n" "$DIAGNOSTIC_TEXT" >&2; fi',
|
||||
// QUERY_EXIT lets a test make the query FAIL (gsd-tools missing, a crash, a
|
||||
// QUERY_EXIT lets a test make the query FAIL (msd-tools missing, a crash, a
|
||||
// non-zero exit). Defaults to 0, so every pre-existing caller of this
|
||||
// harness is unaffected. Nothing is printed on stdout in that case —
|
||||
// matching a real failed command substitution.
|
||||
@@ -772,18 +772,18 @@ describe('#3552: configured protected branches', () => {
|
||||
});
|
||||
|
||||
test('#3552 active workstream CLI uses configured list and excludes root-only names', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3552-cli-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3552-cli-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
setGsdConfig(dir, 'git.protected_branches', ['root-only']);
|
||||
setMsdConfig(dir, 'git.protected_branches', ['root-only']);
|
||||
fs.mkdirSync(path.join(dir, '.planning', 'workstreams', 'alpha'), { recursive: true });
|
||||
// Both HOME and USERPROFILE must be redirected — Node's os.homedir() (and
|
||||
// anything relying on it) consults USERPROFILE first on Windows, where
|
||||
// setting HOME alone leaves the real home directory in effect and makes
|
||||
// this isolation silently vacuous on that platform.
|
||||
const workstreamEnv = { GSD_WORKSTREAM: 'alpha', HOME: dir, USERPROFILE: dir };
|
||||
const workstreamEnv = { MSD_WORKSTREAM: 'alpha', HOME: dir, USERPROFILE: dir };
|
||||
|
||||
const setResult = runGsdTools(
|
||||
const setResult = runMsdTools(
|
||||
['config-set', 'git.protected_branches', '["develop","next"]'],
|
||||
dir,
|
||||
workstreamEnv,
|
||||
@@ -795,13 +795,13 @@ describe('#3552: configured protected branches', () => {
|
||||
));
|
||||
assert.deepStrictEqual(workstreamConfig.git.protected_branches, ['develop', 'next']);
|
||||
|
||||
const match = runGsdTools(
|
||||
const match = runMsdTools(
|
||||
['query', 'git.base-branch', '--is-protected', 'develop'], dir, workstreamEnv,
|
||||
);
|
||||
const control = runGsdTools(
|
||||
const control = runMsdTools(
|
||||
['query', 'git.base-branch', '--is-protected', 'topic/3552'], dir, workstreamEnv,
|
||||
);
|
||||
const rootOnly = runGsdTools(
|
||||
const rootOnly = runMsdTools(
|
||||
['query', 'git.base-branch', '--is-protected', 'root-only'], dir, workstreamEnv,
|
||||
);
|
||||
|
||||
@@ -828,7 +828,7 @@ describe('#3552: configured protected branches', () => {
|
||||
test('#3552 execute-phase warns on true, stays silent on false, and continues both', (t) => {
|
||||
const bash = extractProtectedBranchWarningBash('execute-phase.md', 'handle_branching');
|
||||
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript(
|
||||
'gsd-3552-execute-',
|
||||
'msd-3552-execute-',
|
||||
bash,
|
||||
);
|
||||
t.after(() => cleanup(scriptDir));
|
||||
@@ -856,7 +856,7 @@ describe('#3552: configured protected branches', () => {
|
||||
test('#3552 ship warns on true, stays silent on false, and keeps the none-strategy offer', (t) => {
|
||||
const bash = extractProtectedBranchWarningBash('ship.md', 'preflight_checks');
|
||||
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript(
|
||||
'gsd-3552-ship-',
|
||||
'msd-3552-ship-',
|
||||
bash,
|
||||
);
|
||||
t.after(() => cleanup(scriptDir));
|
||||
@@ -941,11 +941,11 @@ describe('#3552: configured protected branches', () => {
|
||||
});
|
||||
|
||||
test('#3648 cmdGitBaseBranch usage errors emit structured ERROR_REASON.USAGE and do not print stack trace', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-usage-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-usage-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
|
||||
const unknownFlag = runGsdTools(['query', 'git.base-branch', '--unknown-flag', '--json-errors'], dir);
|
||||
const unknownFlag = runMsdTools(['query', 'git.base-branch', '--unknown-flag', '--json-errors'], dir);
|
||||
assert.strictEqual(unknownFlag.success, false);
|
||||
assert.strictEqual(unknownFlag.exitCode, 1);
|
||||
assert.doesNotMatch(unknownFlag.error, /^\s*at\s+/m, 'usage errors must not print stack traces');
|
||||
@@ -953,7 +953,7 @@ describe('#3552: configured protected branches', () => {
|
||||
try { parsed = JSON.parse(unknownFlag.error); } catch {}
|
||||
assert.strictEqual(parsed?.reason, 'usage');
|
||||
|
||||
const surplusPositional = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main', 'extra', '--json-errors'], dir);
|
||||
const surplusPositional = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main', 'extra', '--json-errors'], dir);
|
||||
assert.strictEqual(surplusPositional.success, false);
|
||||
assert.strictEqual(surplusPositional.exitCode, 1);
|
||||
assert.doesNotMatch(surplusPositional.error, /^\s*at\s+/m);
|
||||
@@ -963,7 +963,7 @@ describe('#3552: configured protected branches', () => {
|
||||
});
|
||||
|
||||
test('#3648 Minor F-1: nested and flat predicate reads stay aligned with the loader', () => {
|
||||
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
|
||||
const cases = [
|
||||
{ git: { base_branch: 'nested', protected_branches: ['develop'] }, base_branch: 'flat', protected_branches: ['bogus'] },
|
||||
{ git: { base_branch: 'nested' }, base_branch: 'flat' },
|
||||
@@ -992,8 +992,8 @@ describe('#3552: configured protected branches', () => {
|
||||
// warning on a branch that is not protected. The diagnostic was exercised
|
||||
// only by its unit test — dead in production.
|
||||
const cases = [
|
||||
['execute-phase.md', 'handle_branching', 'gsd-3648-b4-execute-'],
|
||||
['ship.md', 'preflight_checks', 'gsd-3648-b4-ship-'],
|
||||
['execute-phase.md', 'handle_branching', 'msd-3648-b4-execute-'],
|
||||
['ship.md', 'preflight_checks', 'msd-3648-b4-ship-'],
|
||||
];
|
||||
|
||||
for (const [workflow, step, prefix] of cases) {
|
||||
@@ -1029,8 +1029,8 @@ describe('#3552: configured protected branches', () => {
|
||||
});
|
||||
|
||||
test('#3648 round-4: a FAILED query degrades visibly, and does not abort under set -e', (t) => {
|
||||
// Found by the round-4 external review. `IS_PROTECTED=$(gsd_run ...)` had
|
||||
// two problems when the query itself failed (gsd-tools absent, a crash, any
|
||||
// Found by the round-4 external review. `IS_PROTECTED=$(msd_run ...)` had
|
||||
// two problems when the query itself failed (msd-tools absent, a crash, any
|
||||
// non-zero exit): the substitution yielded an empty string, so `[ "$X" = true ]`
|
||||
// was simply false and the workflow continued with NO warning and no trace —
|
||||
// a silent fail-open in the guard whose whole purpose is to warn; and the
|
||||
@@ -1039,10 +1039,10 @@ describe('#3552: configured protected branches', () => {
|
||||
//
|
||||
// The contract is neither fail-open nor fail-closed: it degrades VISIBLY.
|
||||
// Claiming "protected" on no evidence would warn on every branch whenever
|
||||
// gsd-tools is unavailable; claiming "not protected" is the silent hole.
|
||||
// msd-tools is unavailable; claiming "not protected" is the silent hole.
|
||||
const cases = [
|
||||
['execute-phase.md', 'handle_branching', 'gsd-3648-r4-execute-'],
|
||||
['ship.md', 'preflight_checks', 'gsd-3648-r4-ship-'],
|
||||
['execute-phase.md', 'handle_branching', 'msd-3648-r4-execute-'],
|
||||
['ship.md', 'preflight_checks', 'msd-3648-r4-ship-'],
|
||||
];
|
||||
|
||||
for (const [workflow, step, prefix] of cases) {
|
||||
@@ -1094,7 +1094,7 @@ describe('#3552: configured protected branches', () => {
|
||||
// state from warning text in tool output; the comparison it replaced was
|
||||
// directly evaluable.
|
||||
const bash = extractProtectedBranchWarningBash('ship.md', 'preflight_checks');
|
||||
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript('gsd-3648-bind-', bash);
|
||||
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript('msd-3648-bind-', bash);
|
||||
t.after(() => cleanup(scriptDir));
|
||||
|
||||
const baseEnv = { ...process.env, CURRENT_BRANCH_VALUE: 'develop' };
|
||||
@@ -1166,12 +1166,12 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
|
||||
});
|
||||
|
||||
test('#3819 CLI: git.allow_default_branch_commits:true makes --is-protected report false on the base branch', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3819-cli-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3819-cli-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
setGsdConfig(dir, 'git.allow_default_branch_commits', true);
|
||||
setMsdConfig(dir, 'git.allow_default_branch_commits', true);
|
||||
|
||||
const escaped = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main'], dir);
|
||||
const escaped = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main'], dir);
|
||||
assert.ok(escaped.success, escaped.error);
|
||||
assert.strictEqual(escaped.output, 'false',
|
||||
'the escape hatch must make the CLI report the base branch as not protected');
|
||||
@@ -1179,10 +1179,10 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
|
||||
// Negative control: a sibling fixture without the config key must still
|
||||
// report the base branch as protected — proves the flag, not some other
|
||||
// difference between the two fixtures, drives the result above.
|
||||
const controlDir = createGitRepo({ prefix: 'gsd-3819-cli-control-', defaultBranch: 'main' });
|
||||
const controlDir = createGitRepo({ prefix: 'msd-3819-cli-control-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(controlDir));
|
||||
addPlanning(controlDir);
|
||||
const control = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main'], controlDir);
|
||||
const control = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main'], controlDir);
|
||||
assert.ok(control.success, control.error);
|
||||
assert.strictEqual(control.output, 'true');
|
||||
assert.notStrictEqual(escaped.output, control.output,
|
||||
@@ -1190,17 +1190,17 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3819: gsd-executor.md pre-commit protected-branch guard ────────────────
|
||||
// ─── #3819: msd-executor.md pre-commit protected-branch guard ────────────────
|
||||
|
||||
/**
|
||||
* Extract the FIRST ```bash fenced block that follows the "Pre-commit
|
||||
* protected-branch safety assertion" heading in agents/gsd-executor.md.
|
||||
* protected-branch safety assertion" heading in agents/msd-executor.md.
|
||||
* Keyed off the heading text (not a `<step name>` tag — this step has no
|
||||
* such wrapper) so a future rewording of the heading fails the test loudly
|
||||
* instead of silently extracting nothing.
|
||||
*/
|
||||
function extractExecutorPreCommitBash() {
|
||||
const executorPath = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
|
||||
const executorPath = path.join(__dirname, '..', 'agents', 'msd-executor.md');
|
||||
const content = readFileNormalized(executorPath);
|
||||
const lines = content.split('\n');
|
||||
const headingIndex = lines.findIndex(
|
||||
@@ -1208,7 +1208,7 @@ function extractExecutorPreCommitBash() {
|
||||
);
|
||||
if (headingIndex === -1) {
|
||||
throw new Error(
|
||||
'agents/gsd-executor.md: could not find the "Pre-commit HEAD safety ' +
|
||||
'agents/msd-executor.md: could not find the "Pre-commit HEAD safety ' +
|
||||
'assertion" heading — has it been reworded?',
|
||||
);
|
||||
}
|
||||
@@ -1226,13 +1226,13 @@ function extractExecutorPreCommitBash() {
|
||||
if (inBash) buffer.push(line);
|
||||
}
|
||||
throw new Error(
|
||||
'agents/gsd-executor.md: no ```bash block found after the pre-commit protected-branch ' +
|
||||
'agents/msd-executor.md: no ```bash block found after the pre-commit protected-branch ' +
|
||||
'heading — has the step been restructured?',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a standalone script that mocks `git` and `gsd_run`, then runs the
|
||||
* Write a standalone script that mocks `git` and `msd_run`, then runs the
|
||||
* extracted pre-commit guard bash verbatim. Deliberately does NOT `set -e`:
|
||||
* the guard's fatal paths call `exit 1` explicitly, and this script must run
|
||||
* to completion on the non-fatal path to print the GUARD_PASSED sentinel.
|
||||
@@ -1266,10 +1266,10 @@ function writeExecutorGuardScript(prefix, bash, { branch, headRef, isWorktree, q
|
||||
' return 97',
|
||||
' fi',
|
||||
'}',
|
||||
'gsd_run() {',
|
||||
'msd_run() {',
|
||||
` if [ "$#" -ne 4 ] || [ "$1" != query ] || [ "$2" != git.base-branch ] || ` +
|
||||
`[ "$3" != --is-protected ] || [ "$4" != "${branch}" ]; then`,
|
||||
' printf "unexpected gsd_run invocation: %s\\n" "$*" >&2',
|
||||
' printf "unexpected msd_run invocation: %s\\n" "$*" >&2',
|
||||
' return 0',
|
||||
' fi',
|
||||
queryExit
|
||||
@@ -1282,11 +1282,11 @@ function writeExecutorGuardScript(prefix, bash, { branch, headRef, isWorktree, q
|
||||
return { scriptDir, scriptPath };
|
||||
}
|
||||
|
||||
describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
describe('#3819: msd-executor.md pre-commit protected-branch guard', () => {
|
||||
const bash = extractExecutorPreCommitBash();
|
||||
|
||||
test('#3819 non-worktree, branch is protected (query says true) → HALT with exit 1, no GUARD_PASSED', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-nonwt-protected-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-nonwt-protected-', bash, {
|
||||
branch: 'main',
|
||||
headRef: 'main',
|
||||
isWorktree: false,
|
||||
@@ -1302,7 +1302,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
});
|
||||
|
||||
test('#3819 non-worktree, branch is not protected (query says false) → continues, GUARD_PASSED', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-nonwt-clean-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-nonwt-clean-', bash, {
|
||||
branch: 'feature-x',
|
||||
headRef: 'feature-x',
|
||||
isWorktree: false,
|
||||
@@ -1317,7 +1317,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
});
|
||||
|
||||
test('#3819 worktree, branch is protected → HALT with exit 1 (protected check fires before the allow-list check)', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-protected-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-protected-', bash, {
|
||||
branch: 'main',
|
||||
headRef: 'main',
|
||||
isWorktree: true,
|
||||
@@ -1332,7 +1332,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
});
|
||||
|
||||
test('#3819 worktree, branch not protected but outside the agent-*/worktree-agent-*/worktree-wf_* namespace → HALT', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-outside-namespace-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-outside-namespace-', bash, {
|
||||
branch: 'some-random-branch',
|
||||
headRef: 'some-random-branch',
|
||||
isWorktree: true,
|
||||
@@ -1347,7 +1347,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
});
|
||||
|
||||
test('#3819 worktree, branch not protected AND in the agent-* namespace → continues, GUARD_PASSED', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-clean-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-clean-', bash, {
|
||||
branch: 'agent-42',
|
||||
headRef: 'agent-42',
|
||||
isWorktree: true,
|
||||
@@ -1362,7 +1362,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
});
|
||||
|
||||
test('#3819 detached HEAD (non-worktree) → HALT with exit 1 before any protected-branch query', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-detached-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-detached-', bash, {
|
||||
branch: 'HEAD',
|
||||
headRef: 'DETACHED',
|
||||
isWorktree: false,
|
||||
@@ -1376,24 +1376,24 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
assert.match(result.stderr, /detached/);
|
||||
});
|
||||
|
||||
test('#3819 HOSTILE: gsd_run RUNS but reports the branch as protected (resolver-internal fail-closed) → HALT', (t) => {
|
||||
test('#3819 HOSTILE: msd_run RUNS but reports the branch as protected (resolver-internal fail-closed) → HALT', (t) => {
|
||||
// This is the scenario that must stay fail-closed unconditionally:
|
||||
// gsd_run itself succeeds (exit 0) — e.g. because cmdGitBaseBranch's own
|
||||
// msd_run itself succeeds (exit 0) — e.g. because cmdGitBaseBranch's own
|
||||
// pre-existing #3057 B4 logic already answered "true" when it could not
|
||||
// VERIFY the real default branch — and the bash guard must never
|
||||
// second-guess that answer. It is deliberately indistinguishable, at
|
||||
// this bash layer, from an ordinary "yes this branch is protected"
|
||||
// answer (see the "non-worktree, branch is protected" test above) —
|
||||
// that IS the fail-closed contract: the guard trusts what gsd_run says
|
||||
// when gsd_run actually says something.
|
||||
// that IS the fail-closed contract: the guard trusts what msd_run says
|
||||
// when msd_run actually says something.
|
||||
//
|
||||
// This is a DIFFERENT failure mode from "gsd_run could not be invoked at
|
||||
// This is a DIFFERENT failure mode from "msd_run could not be invoked at
|
||||
// all" (nonzero exit / no output), which is covered by the two
|
||||
// "gsd_run itself is unavailable" tests below and — per the #3819
|
||||
// "msd_run itself is unavailable" tests below and — per the #3819
|
||||
// approval's item 2 ("keep the existing names as a fallback where the
|
||||
// default cannot be resolved") — deliberately does NOT fail closed for
|
||||
// every branch; it falls back to the pre-#3819 five-name list instead.
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-resolver-unverified-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-resolver-unverified-', bash, {
|
||||
branch: 'feature-x',
|
||||
headRef: 'feature-x',
|
||||
isWorktree: false,
|
||||
@@ -1408,8 +1408,8 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
assert.doesNotMatch(result.stdout, /GUARD_PASSED/);
|
||||
});
|
||||
|
||||
test('#3819 gsd_run itself is unavailable (query cannot run) + branch matches the old five-name list → still HALTs via the fallback', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-unavailable-listed-', bash, {
|
||||
test('#3819 msd_run itself is unavailable (query cannot run) + branch matches the old five-name list → still HALTs via the fallback', (t) => {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-unavailable-listed-', bash, {
|
||||
branch: 'develop',
|
||||
headRef: 'develop',
|
||||
isWorktree: false,
|
||||
@@ -1424,13 +1424,13 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
assert.match(result.stderr, /protected\/default branch/);
|
||||
});
|
||||
|
||||
test('#3819 gsd_run itself is unavailable (query cannot run) + branch does NOT match the old five-name list → falls back to allowing it', (t) => {
|
||||
// Deliberate #3819-approved tradeoff: when gsd-tools itself cannot even be
|
||||
test('#3819 msd_run itself is unavailable (query cannot run) + branch does NOT match the old five-name list → falls back to allowing it', (t) => {
|
||||
// Deliberate #3819-approved tradeoff: when msd-tools itself cannot even be
|
||||
// invoked, an unlisted branch is allowed through rather than blocking every
|
||||
// branch outright — unlike the "HOSTILE: the protected-branch query itself
|
||||
// fails" test above (gsd-tools runs fine but the resolver's own internal
|
||||
// fails" test above (msd-tools runs fine but the resolver's own internal
|
||||
// verification fails), which stays fail-closed.
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-unavailable-unlisted-', bash, {
|
||||
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-unavailable-unlisted-', bash, {
|
||||
branch: 'my-feature-branch',
|
||||
headRef: 'my-feature-branch',
|
||||
isWorktree: false,
|
||||
@@ -1450,10 +1450,10 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
|
||||
// <final_commit> block itself — the workflow .md IS the product surface here,
|
||||
// same rationale as Guard G (see the top-of-file exemption above).
|
||||
test('#3819 <final_commit> block instructs the executor to re-run the Step 0 guard before the final commit', () => {
|
||||
const executorPath = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
|
||||
const executorPath = path.join(__dirname, '..', 'agents', 'msd-executor.md');
|
||||
const content = readFileNormalized(executorPath);
|
||||
const startIndex = content.indexOf('<final_commit>');
|
||||
assert.notStrictEqual(startIndex, -1, 'agents/gsd-executor.md: could not find <final_commit> tag — has it been renamed?');
|
||||
assert.notStrictEqual(startIndex, -1, 'agents/msd-executor.md: could not find <final_commit> tag — has it been renamed?');
|
||||
const fenceIndex = content.indexOf('```bash', startIndex);
|
||||
const excerpt = fenceIndex === -1
|
||||
? content.slice(startIndex, startIndex + 2000)
|
||||
@@ -1507,7 +1507,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
|
||||
// structural keeps the two definitions of "usable branch name" in step —
|
||||
// only this property, which asks both about the same values.
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
const { isValidProtectedBranches } = require('../gsd-core/bin/lib/config.cjs');
|
||||
const { isValidProtectedBranches } = require('../msd-core/bin/lib/config.cjs');
|
||||
|
||||
/** A value generator weighted towards the boundary cases both surfaces care about. */
|
||||
const entry = () => fc.oneof(
|
||||
@@ -1564,7 +1564,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
|
||||
// BRAND-NEW key through it invents an undocumented top-level spelling that
|
||||
// silently outranks `git.protected_branches`. `protected_branches` has no
|
||||
// legacy form, so it resolves nested-only — in production and in the seam.
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-flat-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-flat-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
fs.writeFileSync(
|
||||
@@ -1572,7 +1572,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
|
||||
JSON.stringify({ protected_branches: ['bogus'], git: { protected_branches: ['develop'] } }),
|
||||
);
|
||||
|
||||
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
|
||||
assert.deepStrictEqual(
|
||||
configLoader.loadConfig(dir, { persist: false })['protected_branches'], ['develop'],
|
||||
'the canonical nested key must win outright');
|
||||
@@ -1645,7 +1645,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
}
|
||||
|
||||
test('a legacy-key config survives --is-protected byte-for-byte, and is still honoured', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-b1-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-b1-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
const cfgPath = writeLegacyConfig(dir);
|
||||
@@ -1653,8 +1653,8 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
const before = fs.readFileSync(cfgPath);
|
||||
const planningBefore = snapshotPlanningTree(dir);
|
||||
|
||||
const match = runGsdTools(['query', 'git.base-branch', '--is-protected', 'develop'], dir);
|
||||
const control = runGsdTools(['query', 'git.base-branch', '--is-protected', 'topic/3648'], dir);
|
||||
const match = runMsdTools(['query', 'git.base-branch', '--is-protected', 'develop'], dir);
|
||||
const control = runMsdTools(['query', 'git.base-branch', '--is-protected', 'topic/3648'], dir);
|
||||
|
||||
assert.ok(match.success, match.error);
|
||||
assert.ok(control.success, control.error);
|
||||
@@ -1680,13 +1680,13 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
// Without this the test above cannot fail for the right reason — a fixture
|
||||
// that never triggered a normalization would keep its bytes no matter what
|
||||
// `persist` did. This proves the fixture is live.
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-b1-neg-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-b1-neg-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
const cfgPath = writeLegacyConfig(dir);
|
||||
const before = fs.readFileSync(cfgPath);
|
||||
|
||||
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
|
||||
const persisted = configLoader.loadConfig(dir);
|
||||
|
||||
assert.notDeepStrictEqual(
|
||||
@@ -1698,7 +1698,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
});
|
||||
|
||||
test('negative control: a planted .planning sibling changes the tree snapshot', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-b1-stray-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-b1-stray-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
const before = snapshotPlanningTree(dir);
|
||||
@@ -1712,13 +1712,13 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
});
|
||||
|
||||
test('persist:false changes only the side effect, not the resolved config', (t) => {
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-b1-parity-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-b1-parity-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
const cfgPath = writeLegacyConfig(dir);
|
||||
const before = fs.readFileSync(cfgPath);
|
||||
|
||||
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
|
||||
const quiet = configLoader.loadConfig(dir, { persist: false });
|
||||
assert.deepStrictEqual(fs.readFileSync(cfgPath), before,
|
||||
'persist:false must not write');
|
||||
@@ -1737,7 +1737,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
// persistence and rewrote the root config — the predicate's `persist:false`
|
||||
// was silently discarded for exactly the projects that use workstreams.
|
||||
// Found by the round-4 external review.
|
||||
const dir = createGitRepo({ prefix: 'gsd-3648-b1-ws-', defaultBranch: 'main' });
|
||||
const dir = createGitRepo({ prefix: 'msd-3648-b1-ws-', defaultBranch: 'main' });
|
||||
t.after(() => cleanup(dir));
|
||||
addPlanning(dir);
|
||||
// The workstream directory exists but carries no config.json — the shape
|
||||
@@ -1747,7 +1747,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
const cfgPath = writeLegacyConfig(dir);
|
||||
const before = fs.readFileSync(cfgPath);
|
||||
|
||||
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
|
||||
const quiet = configLoader.loadConfig(dir, { persist: false, workstream: 'alpha' });
|
||||
|
||||
assert.strictEqual(quiet['base_branch'], 'develop',
|
||||
@@ -1764,7 +1764,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
|
||||
});
|
||||
|
||||
describe('#3648 Major 1: readEffectiveGitConfig readFile seam — config present but unusable', () => {
|
||||
const CWD = path.join(path.sep, 'gsd-3648-seam');
|
||||
const CWD = path.join(path.sep, 'msd-3648-seam');
|
||||
|
||||
/** Drive the seam with raw config text, recording the paths requested. */
|
||||
function readWith(raw, seenPaths) {
|
||||
@@ -2144,7 +2144,7 @@ describe('#3057 W3: gitWorktreeInfoInternal — no work tree, and git failing mi
|
||||
// `git rev-parse --is-inside-work-tree` exits 0 in a bare repo and prints
|
||||
// "false" — the exitCode guard does NOT fire, so this is the stdout check,
|
||||
// and it is reachable without any injection.
|
||||
const dir = createTempDir('gsd-3057-w3-bare-');
|
||||
const dir = createTempDir('msd-3057-w3-bare-');
|
||||
t.after(() => cleanup(dir));
|
||||
gitOrThrow(['init', '--bare'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
|
||||
|
||||
@@ -2253,38 +2253,38 @@ describe('#3057 W3: cmdGitBaseBranch — the DEFAULT diagnostic sink', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── setGsdConfig prototype-pollution guard (#1406) ───────────────────────────
|
||||
// ─── setMsdConfig prototype-pollution guard (#1406) ───────────────────────────
|
||||
|
||||
describe('#1406: setGsdConfig prototype-pollution guard', () => {
|
||||
describe('#1406: setMsdConfig prototype-pollution guard', () => {
|
||||
test('rejects __proto__ as a key segment', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
|
||||
t.after(() => cleanup(dir));
|
||||
assert.throws(() => setGsdConfig(dir, '__proto__', 'x'), /unsafe config key segment/);
|
||||
assert.throws(() => setGsdConfig(dir, '__proto__.polluted', true), /unsafe config key segment/);
|
||||
assert.throws(() => setMsdConfig(dir, '__proto__', 'x'), /unsafe config key segment/);
|
||||
assert.throws(() => setMsdConfig(dir, '__proto__.polluted', true), /unsafe config key segment/);
|
||||
});
|
||||
|
||||
test('rejects constructor / prototype chain segments', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
|
||||
t.after(() => cleanup(dir));
|
||||
assert.throws(() => setGsdConfig(dir, 'constructor.prototype.polluted', true), /unsafe config key segment/);
|
||||
assert.throws(() => setGsdConfig(dir, 'safe.__proto__', true), /unsafe config key segment/);
|
||||
assert.throws(() => setGsdConfig(dir, 'a.prototype.b', true), /unsafe config key segment/);
|
||||
assert.throws(() => setMsdConfig(dir, 'constructor.prototype.polluted', true), /unsafe config key segment/);
|
||||
assert.throws(() => setMsdConfig(dir, 'safe.__proto__', true), /unsafe config key segment/);
|
||||
assert.throws(() => setMsdConfig(dir, 'a.prototype.b', true), /unsafe config key segment/);
|
||||
});
|
||||
|
||||
test('does not pollute Object.prototype after rejected attempts', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
|
||||
t.after(() => cleanup(dir));
|
||||
try { setGsdConfig(dir, '__proto__.polluted', true); } catch (_) { /* expected */ }
|
||||
try { setGsdConfig(dir, 'constructor.prototype.polluted', true); } catch (_) { /* expected */ }
|
||||
try { setGsdConfig(dir, 'a.__proto__.polluted', true); } catch (_) { /* expected */ }
|
||||
try { setMsdConfig(dir, '__proto__.polluted', true); } catch (_) { /* expected */ }
|
||||
try { setMsdConfig(dir, 'constructor.prototype.polluted', true); } catch (_) { /* expected */ }
|
||||
try { setMsdConfig(dir, 'a.__proto__.polluted', true); } catch (_) { /* expected */ }
|
||||
assert.strictEqual(({}).polluted, undefined);
|
||||
assert.strictEqual(Object.prototype.polluted, undefined);
|
||||
});
|
||||
|
||||
test('still writes a normal nested key', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
|
||||
t.after(() => cleanup(dir));
|
||||
setGsdConfig(dir, 'git.base_branch', 'develop');
|
||||
setMsdConfig(dir, 'git.base_branch', 'develop');
|
||||
const cfgPath = path.join(dir, '.planning', 'config.json');
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
||||
assert.strictEqual(cfg.git.base_branch, 'develop');
|
||||
@@ -2301,7 +2301,7 @@ describe('#1406: setGsdConfig prototype-pollution guard', () => {
|
||||
/**
|
||||
* Regression tests for bug #2004
|
||||
*
|
||||
* /gsd-pr-branch must not exclude milestone archive and structural planning
|
||||
* /msd-pr-branch must not exclude milestone archive and structural planning
|
||||
* commits. The previous implementation filtered ALL .planning/-only commits,
|
||||
* including STATE.md, ROADMAP.md, MILESTONES.md, and milestones/** updates
|
||||
* that are needed to preserve repository planning state after a merge.
|
||||
@@ -2322,7 +2322,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const workflowPath = path.resolve(
|
||||
__dirname, '..', 'gsd-core', 'workflows', 'pr-branch.md'
|
||||
__dirname, '..', 'msd-core', 'workflows', 'pr-branch.md'
|
||||
);
|
||||
|
||||
describe('bug #2004: pr-branch preserves structural planning commits', () => {
|
||||
@@ -2427,7 +2427,7 @@ const { cleanup } = require('./helpers.cjs');
|
||||
const EXECUTE_PHASE_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'gsd-core',
|
||||
'msd-core',
|
||||
'workflows',
|
||||
'execute-phase.md'
|
||||
);
|
||||
@@ -2510,7 +2510,7 @@ function extractHandleBranchingBash() {
|
||||
* same treatment in 80f14cac; this test deserves the same coverage).
|
||||
*/
|
||||
function setupFixture(defaultBranch = 'main') {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2916-'));
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2916-'));
|
||||
const seedPath = path.join(root, 'seed');
|
||||
const originPath = path.join(root, 'origin.git');
|
||||
const clonePath = path.join(root, 'clone');
|
||||
@@ -2544,7 +2544,7 @@ function runHandleBranchingStep(bash, cwd, branchName) {
|
||||
// Write the script to a sibling tempdir, not inside the repo — putting it in
|
||||
// `cwd` would create an untracked file that trips `git status --porcelain`
|
||||
// and steers the step into its dirty-tree fallback path.
|
||||
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2916-step-'));
|
||||
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2916-step-'));
|
||||
const scriptPath = path.join(scriptDir, 'handle-branching.sh');
|
||||
const script = `#!/usr/bin/env bash\nset -uo pipefail\nBRANCH_NAME="${branchName}"\n${bash}\n`;
|
||||
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
||||
@@ -2713,7 +2713,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
|
||||
});
|
||||
|
||||
test('verify fails when the PR diff deletes target-tracked planning files', (t) => {
|
||||
const repo = createTempDir('gsd-3679-verify-fail-');
|
||||
const repo = createTempDir('msd-3679-verify-fail-');
|
||||
t.after(() => cleanup(repo));
|
||||
const g = (args) => gitOrThrow(args, { cwd: repo });
|
||||
g(['init', '-q', '-b', 'main']);
|
||||
@@ -2755,7 +2755,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
|
||||
});
|
||||
|
||||
test('verify passes a clean diff with zero deletions', (t) => {
|
||||
const repo = createTempDir('gsd-3679-verify-clean-');
|
||||
const repo = createTempDir('msd-3679-verify-clean-');
|
||||
t.after(() => cleanup(repo));
|
||||
const g = (args) => gitOrThrow(args, { cwd: repo });
|
||||
g(['init', '-q', '-b', 'main']);
|
||||
@@ -2793,7 +2793,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
|
||||
});
|
||||
|
||||
test('create loop preserves pre-existing transient content (#3720 guarantee pinned)', (t) => {
|
||||
const repo = createTempDir('gsd-3679-create-mixed-');
|
||||
const repo = createTempDir('msd-3679-create-mixed-');
|
||||
t.after(() => cleanup(repo));
|
||||
const g = (args) => gitOrThrow(args, { cwd: repo });
|
||||
g(['init', '-q', '-b', 'main']);
|
||||
@@ -2847,7 +2847,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
|
||||
});
|
||||
|
||||
test('create loop preserves planning content on pure-code commits', (t) => {
|
||||
const repo = createTempDir('gsd-3679-create-pure-');
|
||||
const repo = createTempDir('msd-3679-create-pure-');
|
||||
t.after(() => cleanup(repo));
|
||||
const g = (args) => gitOrThrow(args, { cwd: repo });
|
||||
g(['init', '-q', '-b', 'main']);
|
||||
|
||||
Reference in New Issue
Block a user