* test(#3145): bound the installer/runtime cluster onto the process seam Migrates 156 unbounded sync spawn sites across 47 files. Allowlist 120 to 73. Timeouts are sized from evidence already in the tree rather than a house default, because this wave spawns installers rather than git plumbing and an undersized bound does not catch a hang -- it manufactures CI flake, which is worse, since a flake gets re-run instead of investigated. install.test.cjs records a real spawnSync ETIMEDOUT at a 60000ms cap on a loaded bench while another lane passed the same commit in 12.7s, so full installs are bound at 120000ms against that recorded incident. Also adds an auditable escape to the guard's timeout ceiling. The 600000ms cap was set in #3143 from partial evidence, but fragment-single-edit- propagation carries a documented, load-tested 900000ms bound on a run that chains a full build plus eight generators -- the guard would have rejected a correct timeout the moment that file left the allowlist. A value above the ceiling is now permitted only with an inline allow-spawn-timeout-ceiling marker carrying a non-empty reason. It raises the ceiling; it never waives the requirement for a bound, which is asserted directly. install-shared.cjs keeps its hand-rolled assert rather than routing through throwIfFailed: its message embeds both streams, and throwIfFailed carries only a trimmed stderr. The message now also names the outcome, so a bounded timeout reads as such across its 38 importers instead of as expected null to equal 0. * test(#3145): extract class-norm timeouts and correct the build-hooks sizing A pre-PR review found 52 copies of four class-norm timeout constants across this wave. These are not per-suite fixture bindings -- they are shared facts about how long a class of subprocess takes, derived from a recorded bench incident. That norm already moved once (60000 to 120000 after a real ETIMEDOUT), and 52 copies would have drifted the next time it moved. Extracts tests/helpers/timeouts.cjs, where each norm is justified once, and converts the copies. A site that genuinely differs -- a real tsc compile, or regen:derived -- keeps its own local constant with its own justification. Also corrects a misclassification: scripts/build-hooks.js was sized as a build at 120000 in twelve places and 60000 in another, but it compiles and bundles nothing. Its own header says no bundling needed; it copies pre-built files and syntax-checks them with vm. Three different values bounded one script; now there is one. * test(#3145): fix red CI — lint self-match and a Windows chunk overrun Two failures on PR 3176. lint-allow-test-rule-refs read a RuleTester fixture as a real exemption. The fixture exists to prove an unrelated marker does NOT suppress the rule, so it carries that marker's literal text as test data. Split via concatenation, the same idiom no-unbounded-spawn-allowlist.test.cjs already uses for its own self-match problem. The explanatory comment needed the same treatment. The Windows shard 3/3 chunk was killed at its 600000ms budget. Output stopped seven minutes before the kill, so this was an overrun rather than a slow chunk: regenDerivedPropagatesSingleFragmentEditWithNoSecondSourceSurface runs regen:derived bounded at 900000ms, which is larger than the whole chunk budget, so the chunk killer always fires first and it can never complete there. Both the test and that bound predate this change; modifying the file pulled it into the Windows targeted set and exposed it. Skipped on Windows with the reason recorded; the Linux lanes cover it. The 900000 bound and its ceiling marker are unchanged -- they are correct. * test(#3145): refresh the stale test-timings cost table The Windows shard was killed at its 600000ms per-chunk budget. run-tests.cjs packs chunks by measured duration from tests/test-timings.json, and an unknown file falls back to the table's median weight -- advisory by design, but it silently underweights exactly the files that matter. Four of the failing chunk's 22 files were absent from the table, including the two heaviest: fragment-single-edit-propagation.install.test.cjs at 230s (it runs regen:derived) and agent-fragments-emission.install.test.cjs at 79s. Both were weighted as average, so the chunk's total weight read 53.68 against a budget of 60 and the packer produced a single chunk. Regenerated from a passing full-suite run, per the remedy the script itself documents. 700 to 770 entries, 70 added, 0 dropped -- verified, since gen-test-timings.cjs replaces the table wholesale rather than merging. Proven against the real packer: the same 22 files now weigh 103.91 and split into two chunks. No logic, budget, or timeout was changed; raising a budget to make a red gate pass is not a fix. --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -45,13 +45,15 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { RUNTIME_META, installerEnv, walk } = require('./helpers/install-shared.cjs');
|
||||
const { buildOverlayRepo } = require('./helpers/overlay-repo.cjs');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** The agent used as the marker probe. Any agent works — this suite proves the
|
||||
* EMISSION PATH, not this file's own content. gsd-codebase-mapper is the
|
||||
@@ -124,11 +126,12 @@ function spawnInstall(repoRoot, runtime, scope) {
|
||||
const cwd = root;
|
||||
if (scope === 'global') args.push('--global', '--config-dir', root);
|
||||
else args.push('--local');
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
const seamResult = runNode(args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
const result = { status: seamResult.exitCode, stdout: seamResult.stdout, stderr: seamResult.stderr };
|
||||
return { result, root };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const { installerEnv } = require('./helpers/install-shared.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const AGENTS_DIR_NAME = 'agents';
|
||||
const MODEL_PROFILES = require('../gsd-core/bin/lib/model-profiles.cjs').MODEL_PROFILES;
|
||||
const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES);
|
||||
@@ -397,17 +399,16 @@ describe('checkAgentsInstalled: Kimi agents/subagents layout', () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const installResult = spawnSync(
|
||||
process.execPath,
|
||||
const installResult = runNode(
|
||||
[INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'],
|
||||
{
|
||||
cwd: tmpDir,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
assert.strictEqual(
|
||||
installResult.status,
|
||||
installResult.exitCode,
|
||||
0,
|
||||
`Kimi install failed\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`,
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { runMinimalInstall, installerEnv, INSTALL_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
@@ -248,11 +249,11 @@ test('antigravity --global uninstall removes only GSD-owned permissions.allow ru
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const args = [INSTALL_SCRIPT, '--antigravity', '--global', '--config-dir', root];
|
||||
const installResult = spawnSync(process.execPath, args, {
|
||||
encoding: 'utf8',
|
||||
const installResult = runNode(args, {
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
assert.strictEqual(installResult.status, 0, `install failed: ${installResult.stderr}`);
|
||||
assert.strictEqual(installResult.exitCode, 0, `install failed: ${installResult.stderr}`);
|
||||
|
||||
// Seed user-owned data alongside GSD's contributions, post-install.
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
@@ -267,11 +268,11 @@ test('antigravity --global uninstall removes only GSD-owned permissions.allow ru
|
||||
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
||||
|
||||
const uninstallArgs = [INSTALL_SCRIPT, '--antigravity', '--global', '--config-dir', root, '--uninstall'];
|
||||
const uninstallResult = spawnSync(process.execPath, uninstallArgs, {
|
||||
encoding: 'utf8',
|
||||
const uninstallResult = runNode(uninstallArgs, {
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
assert.strictEqual(uninstallResult.status, 0, `uninstall failed: ${uninstallResult.stderr}`);
|
||||
assert.strictEqual(uninstallResult.exitCode, 0, `uninstall failed: ${uninstallResult.stderr}`);
|
||||
|
||||
const settingsAfter = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
assert.deepEqual(settingsAfter.permissions.allow, ['command(git)'], 'GSD allow rules removed, user rule preserved');
|
||||
|
||||
@@ -21,7 +21,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { runMinimalInstall, installerEnv, INSTALL_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
@@ -169,8 +169,8 @@ test('augment --global: reinstalling does not duplicate or clobber the gsd MCP c
|
||||
const args = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
||||
const env = installerEnv({ HOME: root, USERPROFILE: root });
|
||||
|
||||
const first = spawnSync(process.execPath, args, { encoding: 'utf8', env });
|
||||
assert.strictEqual(first.status, 0, `first install failed: ${first.stderr}`);
|
||||
const first = runNode(args, { env, timeoutMs: 120000 });
|
||||
assert.strictEqual(first.exitCode, 0, `first install failed: ${first.stderr}`);
|
||||
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
const afterFirst = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
@@ -179,8 +179,8 @@ test('augment --global: reinstalling does not duplicate or clobber the gsd MCP c
|
||||
afterFirst.mcpServers.gsd.args.push('--custom-flag');
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(afterFirst, null, 2) + '\n');
|
||||
|
||||
const second = spawnSync(process.execPath, args, { encoding: 'utf8', env });
|
||||
assert.strictEqual(second.status, 0, `second install failed: ${second.stderr}`);
|
||||
const second = runNode(args, { env, timeoutMs: 120000 });
|
||||
assert.strictEqual(second.exitCode, 0, `second install failed: ${second.stderr}`);
|
||||
|
||||
const afterSecond = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
assert.equal(Object.keys(afterSecond.mcpServers).length, 1, 'reinstall must not duplicate the gsd entry');
|
||||
@@ -201,11 +201,11 @@ test('augment --global: installing preserves a pre-existing unrelated mcpServers
|
||||
}, null, 2));
|
||||
|
||||
const args = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
encoding: 'utf8',
|
||||
const result = runNode(args, {
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
assert.strictEqual(result.status, 0, `install failed: ${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0, `install failed: ${result.stderr}`);
|
||||
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(root, 'settings.json'), 'utf8'));
|
||||
assert.deepEqual(settings.mcpServers.other, { command: 'my-tool', args: ['--flag'] },
|
||||
@@ -223,8 +223,8 @@ test('augment --global uninstall removes only the GSD-owned mcpServers.gsd entry
|
||||
|
||||
const env = installerEnv({ HOME: root, USERPROFILE: root });
|
||||
const installArgs = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
||||
const installResult = spawnSync(process.execPath, installArgs, { encoding: 'utf8', env });
|
||||
assert.strictEqual(installResult.status, 0, `install failed: ${installResult.stderr}`);
|
||||
const installResult = runNode(installArgs, { env, timeoutMs: 120000 });
|
||||
assert.strictEqual(installResult.exitCode, 0, `install failed: ${installResult.stderr}`);
|
||||
|
||||
// Seed user-owned data alongside GSD's contributions, post-install.
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
@@ -233,8 +233,8 @@ test('augment --global uninstall removes only the GSD-owned mcpServers.gsd entry
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
const uninstallArgs = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root, '--uninstall'];
|
||||
const uninstallResult = spawnSync(process.execPath, uninstallArgs, { encoding: 'utf8', env });
|
||||
assert.strictEqual(uninstallResult.status, 0, `uninstall failed: ${uninstallResult.stderr}`);
|
||||
const uninstallResult = runNode(uninstallArgs, { env, timeoutMs: 120000 });
|
||||
assert.strictEqual(uninstallResult.exitCode, 0, `uninstall failed: ${uninstallResult.stderr}`);
|
||||
|
||||
const settingsAfter = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
assert.equal(settingsAfter.mcpServers && settingsAfter.mcpServers.gsd, undefined, 'gsd MCP entry removed');
|
||||
|
||||
@@ -1008,8 +1008,13 @@ describe('capability consent store (#1459)', () => {
|
||||
const capId = 'fifo-list-cap';
|
||||
const dir = path.join(cwd, '.gsd', 'capabilities', capId);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(dir, 'capability.json')]);
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const fifoPath = path.join(dir, 'capability.json');
|
||||
throwIfFailed(
|
||||
runHook(fifoPath, [], { interpreter: 'mkfifo', timeoutMs: 15000 }),
|
||||
`mkfifo ${fifoPath}`,
|
||||
);
|
||||
// A committed project ledger so the list iterates this entry (the FIFO is on the metadata-read path).
|
||||
fs.writeFileSync(path.join(cwd, '.gsd-capabilities.json'), JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
|
||||
@@ -27,8 +27,19 @@ const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const consent = require('../gsd-core/bin/lib/capability-consent.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** Create a FIFO at `fifoPath` via `mkfifo`, throwing on failure. */
|
||||
function mkfifo(fifoPath) {
|
||||
const r = runHook(fifoPath, [], { interpreter: 'mkfifo', timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(r, `mkfifo ${fifoPath}`);
|
||||
}
|
||||
|
||||
function tmpDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix || 'cap-consent-test-'));
|
||||
}
|
||||
@@ -144,8 +155,7 @@ test('bundleContentHash: refuses to follow a symlink in the bundle (fail closed)
|
||||
test('bundleContentHash: refuses a non-regular (FIFO) entry in the bundle (fail closed)', { skip: process.platform === 'win32' }, () => {
|
||||
const dir = makeBundle({ manifest: { id: 'cap', role: 'feature', version: '1.0.0' } });
|
||||
try {
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(dir, 'fifo')]);
|
||||
mkfifo(path.join(dir, 'fifo'));
|
||||
assert.throws(() => consent.bundleContentHash(dir), /non-regular/i, 'a FIFO in the bundle is rejected');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
@@ -504,8 +514,7 @@ test('readConsentStore: a FIFO at the store path does not block; returns empty',
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [consent.consentStorePath(home)]);
|
||||
mkfifo(consent.consentStorePath(home));
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} });
|
||||
} finally {
|
||||
cleanup(home);
|
||||
|
||||
@@ -25,12 +25,14 @@ const {
|
||||
// Destructure optional exports (new in this patch) — will be undefined until implemented.
|
||||
const { LedgerIOError, isValidLedgerEntry, readLedgerStrict, readSmallRegularFile } = capLedger;
|
||||
|
||||
const cp = require('node:child_process');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
// A single `mkfifo` system call creating a fixture FIFO — no install or build.
|
||||
const MKFIFO_TIMEOUT_MS = 15000;
|
||||
/** POSIX-only: make a FIFO at `p` (skips/returns false where mkfifo is unavailable). */
|
||||
function tryMkfifo(p) {
|
||||
if (process.platform === 'win32') return false;
|
||||
const res = cp.spawnSync('mkfifo', [p], { stdio: 'ignore' });
|
||||
return res.status === 0;
|
||||
const res = runHook(p, [], { interpreter: 'mkfifo', timeoutMs: MKFIFO_TIMEOUT_MS });
|
||||
return res.exitCode === 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -26,12 +26,14 @@ const { CAP_MARKER } = lifecycle;
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const cp = require('node:child_process');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
/** POSIX-only: make a FIFO at `p` (returns false where mkfifo is unavailable). */
|
||||
function tryMkfifoLife(p) {
|
||||
if (process.platform === 'win32') return false;
|
||||
const res = cp.spawnSync('mkfifo', [p], { stdio: 'ignore' });
|
||||
return res.status === 0;
|
||||
const res = runHook(p, [], { interpreter: 'mkfifo', timeoutMs: PROBE_TIMEOUT_MS });
|
||||
return res.exitCode === 0;
|
||||
}
|
||||
|
||||
const cleanups = [];
|
||||
|
||||
@@ -16,12 +16,23 @@ const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { loadRegistry, _setValidatorForTest, _setGeneratorForTest } = require('../gsd-core/bin/lib/capability-loader.cjs');
|
||||
const baseRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
const { buildRegistry } = require('../scripts/gen-capability-registry.cjs');
|
||||
|
||||
const HOST = '1.6.0';
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** Create a FIFO at `fifoPath` via `mkfifo`, throwing on failure. */
|
||||
function mkfifo(fifoPath) {
|
||||
const r = runHook(fifoPath, [], { interpreter: 'mkfifo', timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(r, `mkfifo ${fifoPath}`);
|
||||
}
|
||||
|
||||
function featureCap(id, extra) {
|
||||
return {
|
||||
id, role: 'feature', version: '1.0.0', title: id, description: 'overlay cap',
|
||||
@@ -598,8 +609,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
const { proj, home, writeCap } = projectFixture();
|
||||
t.after(() => { cleanup(proj); cleanup(home); });
|
||||
writeCap(featureCap('fifo-ledger-cap', { skills: ['fifo-skill'] }));
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(proj, '.gsd-capabilities.json')]);
|
||||
mkfifo(path.join(proj, '.gsd-capabilities.json'));
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['fifo-ledger-cap'], 'FIFO ledger → no committed ids → inactive');
|
||||
@@ -796,8 +806,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
t.after(() => cleanup(home));
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'fifo-manifest');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(dir, 'capability.json')]);
|
||||
mkfifo(path.join(dir, 'capability.json'));
|
||||
// Co-located GLOBAL committed ledger so the cap is on the hot path (committed → manifest read reached).
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'fifo-manifest': { id: 'fifo-manifest', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
let reg;
|
||||
@@ -866,8 +875,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
});
|
||||
const dir = writeCap(cap);
|
||||
// The fragment.path points at a FIFO INSIDE the cap dir (passes the escape guard; only the READ hangs).
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(dir, 'frag.md')]);
|
||||
mkfifo(path.join(dir, 'frag.md'));
|
||||
// A committed in-repo ledger marks it committed — but the user never consented HERE.
|
||||
writeProjectLedger(proj, [{ id: 'conv1-fifo-frag', integrity: '' }]);
|
||||
// No consent record written.
|
||||
@@ -892,8 +900,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'conv1b-fifo-frag');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
execFileSync('mkfifo', [path.join(dir, 'frag.md')]);
|
||||
mkfifo(path.join(dir, 'frag.md'));
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['conv1b-fifo-frag'], 'a global overlay with a FIFO fragment is inactive (materialize fails closed)');
|
||||
|
||||
@@ -13,7 +13,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
@@ -299,31 +299,30 @@ describe('capability-writer: setCapabilityState', () => {
|
||||
fs.writeFileSync(path.join(planningDir, 'config.json'), '{}');
|
||||
|
||||
const gsdToolsBin = path.resolve(__dirname, '../gsd-core/bin/gsd-tools.cjs');
|
||||
const nodeExe = process.execPath;
|
||||
|
||||
// Test: capability set ui --off (--config-dir rcd) exits 0
|
||||
const offResult = spawnSync(nodeExe, [gsdToolsBin, 'capability', 'set', 'ui', '--off', '--config-dir', rcd], {
|
||||
encoding: 'utf8',
|
||||
const offResult = runNode([gsdToolsBin, 'capability', 'set', 'ui', '--off', '--config-dir', rcd], {
|
||||
cwd,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
assert.equal(offResult.status, 0,
|
||||
`capability set ui --off should exit 0, got ${String(offResult.status)}. stderr: ${offResult.stderr}`);
|
||||
assert.equal(offResult.exitCode, 0,
|
||||
`capability set ui --off should exit 0, got ${String(offResult.exitCode)}. stderr: ${offResult.stderr}`);
|
||||
|
||||
// Test: capability set ui --on (--config-dir rcd) exits 0
|
||||
const onResult = spawnSync(nodeExe, [gsdToolsBin, 'capability', 'set', 'ui', '--on', '--config-dir', rcd], {
|
||||
encoding: 'utf8',
|
||||
const onResult = runNode([gsdToolsBin, 'capability', 'set', 'ui', '--on', '--config-dir', rcd], {
|
||||
cwd,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
assert.equal(onResult.status, 0,
|
||||
`capability set ui --on should exit 0, got ${String(onResult.status)}. stderr: ${onResult.stderr}`);
|
||||
assert.equal(onResult.exitCode, 0,
|
||||
`capability set ui --on should exit 0, got ${String(onResult.exitCode)}. stderr: ${onResult.stderr}`);
|
||||
|
||||
// Test: unknown id exits non-zero
|
||||
const unknownResult = spawnSync(nodeExe, [gsdToolsBin, 'capability', 'set', 'does-not-exist', '--off', '--config-dir', rcd], {
|
||||
encoding: 'utf8',
|
||||
const unknownResult = runNode([gsdToolsBin, 'capability', 'set', 'does-not-exist', '--off', '--config-dir', rcd], {
|
||||
cwd,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
assert.notEqual(unknownResult.status, 0,
|
||||
`capability set does-not-exist --off should exit non-zero, got ${String(unknownResult.status)}`);
|
||||
assert.notEqual(unknownResult.exitCode, 0,
|
||||
`capability set does-not-exist --off should exit non-zero, got ${String(unknownResult.exitCode)}`);
|
||||
} finally {
|
||||
cleanup(rcd);
|
||||
cleanup(cwd);
|
||||
@@ -495,16 +494,14 @@ describe('capability-writer: setCapabilityState', () => {
|
||||
fs.writeFileSync(path.join(planningDir, 'config.json'), '{}');
|
||||
|
||||
const gsdToolsBin = path.resolve(__dirname, '../gsd-core/bin/gsd-tools.cjs');
|
||||
const nodeExe = process.execPath;
|
||||
|
||||
const conflictResult = spawnSync(
|
||||
nodeExe,
|
||||
const conflictResult = runNode(
|
||||
[gsdToolsBin, 'capability', 'set', 'ui', '--on', '--off', '--config-dir', rcd],
|
||||
{ encoding: 'utf8', cwd },
|
||||
{ cwd, timeoutMs: 15000 },
|
||||
);
|
||||
assert.notEqual(
|
||||
conflictResult.status, 0,
|
||||
`capability set ui --on --off should exit non-zero, got ${String(conflictResult.status)}. stderr: ${conflictResult.stderr}`,
|
||||
conflictResult.exitCode, 0,
|
||||
`capability set ui --on --off should exit non-zero, got ${String(conflictResult.exitCode)}. stderr: ${conflictResult.stderr}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(rcd);
|
||||
|
||||
@@ -26,10 +26,8 @@ const SCRIPT = path.join(ROOT, 'scripts', 'ci-rebase-check.cjs');
|
||||
const NODE = process.execPath;
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (init/clone/config/checkout/add/commit) on a small
|
||||
// mkdtemp fixture repo — far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a small inline Node snippet that requires the run() helper
|
||||
|
||||
@@ -18,7 +18,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const fc = require('fast-check');
|
||||
const { CLAUDE_AGENT_ALIASES } = require('../gsd-core/bin/lib/model-resolver.cjs');
|
||||
@@ -32,12 +33,16 @@ const { CLAUDE_AGENT_ALIASES } = require('../gsd-core/bin/lib/model-resolver.cjs
|
||||
// Build on demand so the test passes regardless of runner ordering.
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
before(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2976,19 +2981,24 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const { install, GSD_CODEX_MARKER } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// Ensure hooks/dist/ is populated before install tests
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('#2698: CRLF stale gsd-update-check block is removed on Codex reinstall', () => {
|
||||
@@ -4788,7 +4798,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const { parseTomlToObject, validateCodexConfigSchema, install } = require('../bin/install.js');
|
||||
@@ -4803,9 +4814,16 @@ if (previousGsdTestMode === undefined) {
|
||||
// Ensure hooks/dist/ is populated — mirrors the pattern used by codex-config.test.cjs.
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
before(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5314,7 +5332,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { validateCodexConfigSchema, install } = require('../bin/install.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
@@ -5329,9 +5348,16 @@ if (previousGsdTestMode === undefined) {
|
||||
const { before, beforeEach, afterEach } = require('node:test');
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
before(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6347,13 +6373,18 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { install, uninstall, parseTomlToObject } = require('../bin/install.js');
|
||||
const { createTempDir, cleanup, parseFrontmatter } = require('./helpers.cjs');
|
||||
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
@@ -6399,7 +6430,10 @@ describe('#3427 + #3433 — Codex installer avoids duplicate skills and mixed ho
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
tmpRoot = createTempDir('gsd-3427-3433-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
@@ -6561,13 +6595,18 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
const { createTempDir, cleanup, parseFrontmatter } = require('./helpers.cjs');
|
||||
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
@@ -6601,7 +6640,10 @@ describe('#3562 — Codex install produces discoverable $gsd-* skill surface', {
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
tmpRoot = createTempDir('gsd-3562-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
@@ -6723,13 +6765,18 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { install, uninstall, parseTomlToObject } = require('../bin/install.js');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
@@ -6776,7 +6823,10 @@ describe('#3566 — Codex feature flag is canonical "hooks" (not legacy "codex_h
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
tmpRoot = createTempDir('gsd-3566-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
@@ -7445,7 +7495,8 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
install,
|
||||
@@ -7455,6 +7506,10 @@ const { createTempDir, cleanup, captureConsole } = require('./helpers.cjs');
|
||||
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const prev = process.env.CODEX_HOME;
|
||||
@@ -7488,7 +7543,10 @@ describe('#570 — Codex leak scanner sub-bugs', { concurrency: false }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
tmpRoot = createTempDir('gsd-570-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
|
||||
@@ -231,10 +231,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createTempGitProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (diff/rev-parse) on a small mkdtemp fixture repo —
|
||||
// far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// Repo root resolution. This test file lives in `<repo>/tests/`. Use a single
|
||||
// parent reference (the established repo-wide pattern, e.g. tests/helpers.cjs
|
||||
|
||||
@@ -15,10 +15,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createTempGitProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (add/commit/diff) on a small mkdtemp fixture repo —
|
||||
// far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
describe('commit --files: missing files must not stage deletions (#2014)', () => {
|
||||
let tmpDir;
|
||||
|
||||
@@ -17,10 +17,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createTempGitProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (add/commit/diff/status/rev-list) on a small mkdtemp
|
||||
// fixture repo — far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
describe('commit --files: pathspec honors declared scope (#2112)', () => {
|
||||
let tmpDir;
|
||||
|
||||
@@ -1111,21 +1111,22 @@ describe('Copilot lifecycle hook config (#786)', () => {
|
||||
});
|
||||
|
||||
test('executing the bash hook body produces valid sessionStart JSON', { skip: process.platform === 'win32' }, () => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const [entry] = buildCopilotHookConfig().hooks.sessionStart;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hook-exec-'));
|
||||
try {
|
||||
// No .planning/STATE.md → absent branch
|
||||
const outAbsent = execFileSync('bash', ['-c', entry.bash], { cwd: tmp, encoding: 'utf8' });
|
||||
const parsedAbsent = JSON.parse(outAbsent);
|
||||
const rAbsent = runHook('-c', [entry.bash], { interpreter: 'bash', cwd: tmp, timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(rAbsent, 'bash -c <sessionStart hook body> (absent branch)');
|
||||
const parsedAbsent = JSON.parse(rAbsent.stdout);
|
||||
assert.ok(typeof parsedAbsent.additionalContext === 'string', 'absent branch yields additionalContext string');
|
||||
assert.ok(/gsd-new-project/.test(parsedAbsent.additionalContext), 'absent branch suggests gsd-new-project');
|
||||
|
||||
// With .planning/STATE.md → present branch
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, '.planning', 'STATE.md'), '# state\n');
|
||||
const outPresent = execFileSync('bash', ['-c', entry.bash], { cwd: tmp, encoding: 'utf8' });
|
||||
const parsedPresent = JSON.parse(outPresent);
|
||||
const rPresent = runHook('-c', [entry.bash], { interpreter: 'bash', cwd: tmp, timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(rPresent, 'bash -c <sessionStart hook body> (present branch)');
|
||||
const parsedPresent = JSON.parse(rPresent.stdout);
|
||||
assert.ok(/STATE\.md present/.test(parsedPresent.additionalContext), 'present branch references STATE.md');
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
@@ -1347,8 +1348,9 @@ describe('Copilot manifest and patches fixes', () => {
|
||||
// E2E Integration Tests — Copilot Install & Uninstall
|
||||
// ============================================================================
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const { runNode, runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const EXPECTED_SKILLS = fs.readdirSync(path.join(__dirname, '..', 'commands', 'gsd'))
|
||||
@@ -1356,26 +1358,31 @@ const EXPECTED_SKILLS = fs.readdirSync(path.join(__dirname, '..', 'commands', 'g
|
||||
// Source-roster count (gsd-*.md basenames) — shared helper.
|
||||
const EXPECTED_AGENTS = listAgentFiles().length;
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS, INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function runCopilotInstall(cwd) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath, [INSTALL_PATH, '--copilot', '--local', '--no-sdk'], {
|
||||
const r = runNode([INSTALL_PATH, '--copilot', '--local', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --copilot --local --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
function runCopilotUninstall(cwd) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath, [INSTALL_PATH, '--copilot', '--local', '--uninstall', '--no-sdk'], {
|
||||
const r = runNode([INSTALL_PATH, '--copilot', '--local', '--uninstall', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --copilot --local --uninstall --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
describe('E2E: Copilot full install verification', () => {
|
||||
@@ -1668,25 +1675,23 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
function runCopilotInstallGlobal(cwd, configDir) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath,
|
||||
[INSTALL_PATH, '--copilot', '--global', '--config-dir', configDir, '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
});
|
||||
const r = runNode(
|
||||
[INSTALL_PATH, '--copilot', '--global', '--config-dir', configDir, '--no-sdk'],
|
||||
{ cwd, env, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --copilot --global --config-dir ${configDir} --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
function runCopilotUninstallGlobal(cwd, configDir) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath,
|
||||
[INSTALL_PATH, '--copilot', '--global', '--config-dir', configDir, '--uninstall', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
});
|
||||
const r = runNode(
|
||||
[INSTALL_PATH, '--copilot', '--global', '--config-dir', configDir, '--uninstall', '--no-sdk'],
|
||||
{ cwd, env, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --copilot --global --config-dir ${configDir} --uninstall --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
describe('E2E: Copilot global install (#786)', () => {
|
||||
@@ -1731,23 +1736,25 @@ describe('E2E: Copilot global install (#786)', () => {
|
||||
function runClaudeInstall(cwd) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath, [INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
|
||||
const r = runNode([INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --claude --local --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
function runClaudeUninstall(cwd) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(process.execPath, [INSTALL_PATH, '--claude', '--local', '--uninstall', '--no-sdk'], {
|
||||
const r = runNode([INSTALL_PATH, '--claude', '--local', '--uninstall', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --claude --local --uninstall --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
describe('Claude uninstall preserves user-generated files (#1423)', () => {
|
||||
|
||||
@@ -33,7 +33,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -50,9 +51,12 @@ const DESC = path.join(__dirname, '..', 'capabilities', 'antigravity', 'capabili
|
||||
const ANTIGRAVITY_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const ANTIGRAVITY_AXES = ANTIGRAVITY_CAP.runtime.hostIntegration;
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
test('Antigravity classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
|
||||
@@ -21,7 +21,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -36,9 +37,13 @@ const DESC = path.join(__dirname, '..', 'capabilities', 'augment', 'capability.j
|
||||
const AUGMENT_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const AUGMENT_AXES = AUGMENT_CAP.runtime.hostIntegration;
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
test('Augment classifies as the declarative-cli profile (profileOf)', () => {
|
||||
|
||||
@@ -38,7 +38,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -54,10 +55,17 @@ const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-sha
|
||||
const DESC = path.join(__dirname, '..', 'capabilities', 'codebuddy', 'capability.json');
|
||||
const CODEBUDDY_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const CODEBUDDY_AXES = CODEBUDDY_CAP.runtime.hostIntegration;
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('CodeBuddy classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
|
||||
@@ -43,7 +43,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -60,9 +61,12 @@ const DESC = path.join(__dirname, '..', 'capabilities', 'copilot', 'capability.j
|
||||
const COPILOT_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const COPILOT_AXES = COPILOT_CAP.runtime.hostIntegration;
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
test('Copilot classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
|
||||
@@ -40,7 +40,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -61,9 +62,13 @@ const DESC = path.join(__dirname, '..', 'capabilities', 'windsurf', 'capability.
|
||||
const WINDSURF_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const WINDSURF_AXES = WINDSURF_CAP.runtime.hostIntegration;
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
test('Windsurf classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
|
||||
@@ -35,7 +35,8 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
@@ -51,10 +52,17 @@ const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-sha
|
||||
const DESC = path.join(__dirname, '..', 'capabilities', 'zcode', 'capability.json');
|
||||
const ZCODE_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const ZCODE_AXES = ZCODE_CAP.runtime.hostIntegration;
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('ZCode classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
@@ -44,16 +44,11 @@ describe('#2071: effort sync runs in an installed runtime (no package-root bin/i
|
||||
);
|
||||
|
||||
const gsdTools = path.join(configDir, 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
let combined = '';
|
||||
try {
|
||||
combined = execFileSync(
|
||||
process.execPath,
|
||||
[gsdTools, 'effort', 'sync', '--config-dir', configDir],
|
||||
{ cwd: root, encoding: 'utf-8', env: { ...process.env, HOME: root } },
|
||||
);
|
||||
} catch (e) {
|
||||
combined = `${e.stdout || ''}${e.stderr || ''}${e.message || ''}`;
|
||||
}
|
||||
const result = runNode(
|
||||
[gsdTools, 'effort', 'sync', '--config-dir', configDir],
|
||||
{ cwd: root, env: { ...process.env, HOME: root }, timeoutMs: 15000 },
|
||||
);
|
||||
const combined = `${result.stdout || ''}${result.stderr || ''}`;
|
||||
|
||||
assert.doesNotMatch(
|
||||
combined,
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
|
||||
const { test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
@@ -34,11 +35,15 @@ const {
|
||||
} = require('./helpers/install-shared.cjs');
|
||||
const { evaluateEmittedCaps, formatCapReport } = require('./helpers/emitted-caps.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (DEFECT.HOOKS-DIST-SCOPED-CI). Build it
|
||||
// idempotently before the shared real-install fixture, mirroring
|
||||
// tests/emitted-sizes.test.cjs.
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── Shared real LOCAL windsurf install, built once ───────────────────────────
|
||||
|
||||
@@ -16,7 +16,8 @@ const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
@@ -28,6 +29,10 @@ const {
|
||||
} = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
// 180000: a build — this driver invokes ensureRuntimeBuild against a REAL
|
||||
// tsc compile (not a query), at the tsc-compile end of the
|
||||
// 120000-180000 build-class norm.
|
||||
const BUILD_TIMEOUT_MS = 180000;
|
||||
|
||||
/** Fresh isolated package dir (auto-cleaned) with a libDir and optional tsconfig. */
|
||||
function makeEnv(t, { withTsconfig = true, prebuilt = false } = {}) {
|
||||
@@ -333,8 +338,9 @@ test('integration: real tsc heals a missing lib end-to-end (subprocess)', (t) =>
|
||||
);
|
||||
|
||||
assert.ok(!isBuilt(path.join(dir, 'gsd-core', 'bin', 'lib')), 'lib starts empty');
|
||||
const out = execFileSync(process.execPath, [driver], { encoding: 'utf8', cwd: dir });
|
||||
const parsed = JSON.parse(out);
|
||||
const driverResult = runNode([driver], { cwd: dir, timeoutMs: BUILD_TIMEOUT_MS });
|
||||
throwIfFailed(driverResult, `node ${driver}`);
|
||||
const parsed = JSON.parse(driverResult.stdout);
|
||||
assert.equal(parsed.healed, true, 'self-healed the missing build');
|
||||
assert.equal(parsed.run, 'ok', 'the freshly-compiled cli-exit.cjs loads and runs');
|
||||
});
|
||||
|
||||
@@ -55,6 +55,8 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { cleanup, readFileNormalized } = require('./helpers.cjs');
|
||||
const { RUNTIME_META, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
@@ -215,11 +217,12 @@ function installOverlay(overlayRoot, runtime, extraArgs = []) {
|
||||
root,
|
||||
...extraArgs,
|
||||
];
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
const result = runNode(args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
assert.equal(
|
||||
result.status,
|
||||
0,
|
||||
@@ -250,11 +253,12 @@ function installOverlayExpectingFailure(overlayRoot, runtime, extraArgs = []) {
|
||||
root,
|
||||
...extraArgs,
|
||||
];
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
const result = runNode(args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
return { configDir: root, root, result };
|
||||
}
|
||||
|
||||
@@ -268,11 +272,13 @@ function installOverlayExpectingFailure(overlayRoot, runtime, extraArgs = []) {
|
||||
*/
|
||||
function runOverlayCheck(overlayRoot, scriptRelPath, extraArgs = []) {
|
||||
const scriptPath = path.join(overlayRoot, ...scriptRelPath.split('/'));
|
||||
return spawnSync(process.execPath, [scriptPath, '--check', ...extraArgs], {
|
||||
const result = runNode([scriptPath, '--check', ...extraArgs], {
|
||||
cwd: overlayRoot,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv(),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,11 +290,13 @@ function runOverlayCheck(overlayRoot, scriptRelPath, extraArgs = []) {
|
||||
*/
|
||||
function runOverlayGenerate(overlayRoot, scriptRelPath) {
|
||||
const scriptPath = path.join(overlayRoot, ...scriptRelPath.split('/'));
|
||||
return spawnSync(process.execPath, [scriptPath], {
|
||||
const result = runNode([scriptPath], {
|
||||
cwd: overlayRoot,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv(),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,14 +319,11 @@ function runOverlayGenerate(overlayRoot, scriptRelPath) {
|
||||
* config (unlike `git config --global --add safe.directory`).
|
||||
*/
|
||||
function trackedFileSet(repoRoot) {
|
||||
const lsFiles = spawnSync('git', ['-c', 'safe.directory=*', 'ls-files'], {
|
||||
const stdout = gitOrThrow(['-c', 'safe.directory=*', 'ls-files'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
if (lsFiles.status !== 0) {
|
||||
throw new Error(`git ls-files must succeed in ${repoRoot}\nstderr: ${lsFiles.stderr}`);
|
||||
}
|
||||
return lsFiles.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
return stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1165,7 +1170,13 @@ function isEnvDependentGeneratedPath(rel) {
|
||||
return ENV_DEPENDENT_GENERATED_PATHS.some((prefix) => rel.startsWith(prefix));
|
||||
}
|
||||
|
||||
test('regenDerivedPropagatesSingleFragmentEditWithNoSecondSourceSurface', (t) => {
|
||||
test('regenDerivedPropagatesSingleFragmentEditWithNoSecondSourceSurface', {
|
||||
skip: process.platform === 'win32'
|
||||
? 'regen:derived (full build + 8 generators) is bounded at 900000ms, which exceeds the '
|
||||
+ '600000ms per-chunk CI budget — the chunk killer always fires first, so this can never '
|
||||
+ 'complete on the Windows lane. Covered on the Linux lanes. See #3145.'
|
||||
: false,
|
||||
}, (t) => {
|
||||
// 1. COPY-mode overlay — the whole point of this row over rows 1-20: every
|
||||
// leaf is a real independent inode (see overlay-repo.cjs's opts.mode doc),
|
||||
// so the REAL `--write` chain below can run to completion without ever
|
||||
@@ -1187,6 +1198,10 @@ test('regenDerivedPropagatesSingleFragmentEditWithNoSecondSourceSurface', (t) =>
|
||||
// completed run on a loaded bench (linux-node22), not from a real hang.
|
||||
// 900_000 (15min) is deliberately generous so this can never again flake
|
||||
// on load while still catching a true hang.
|
||||
// allow-spawn-timeout-ceiling: regen:derived is a full build plus eight
|
||||
// generators; 300_000 was observed killing a genuinely-completed run
|
||||
// near the end on a loaded bench, not a real hang, so 900_000 is
|
||||
// deliberately above the 600000 ceiling to never repeat that flake.
|
||||
timeout: 900000,
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
|
||||
@@ -22,7 +22,9 @@ const { describe, test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const { spawnSync, execFileSync } = require('node:child_process');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
@@ -30,10 +32,13 @@ const { runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cj
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const INSTALL_JS = path.join(ROOT, 'bin', 'install.js');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored + built; build it idempotently so a real install
|
||||
// emits hooks (mirrors golden-install-parity / install-minimal-hooks).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -34,13 +34,8 @@ const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
|
||||
const { gitOrThrow, throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
|
||||
/**
|
||||
* Bound for every subprocess in this file: git plumbing (init/config/add/
|
||||
* commit/clone/symbolic-ref/remote/branch) against small mkdtemp fixture
|
||||
* repos, plus the one short handle_branching bash-script run below — all
|
||||
* orders of magnitude under this. #3144.
|
||||
*/
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -23,17 +23,22 @@ const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { RUNTIME_META, runMinimalInstall, BUILD_SCRIPT, buildInstallTree } = require('./helpers/install-shared.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// hooks/dist is gitignored and built (DEFECT.HOOKS-DIST-SCOPED-CI). The scoped
|
||||
// CI test lane does not run build:hooks, so a real install there emits no hooks/
|
||||
// dir — making the harness report "removed (N) hooks/…". Build it idempotently
|
||||
// here so the harness is lane-independent (mirrors golden-install-parity.test.cjs).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
const UPDATE = process.env.UPDATE_INSTALL_TREE === '1';
|
||||
|
||||
@@ -17,9 +17,10 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { runNode } = require('./process-seam.cjs');
|
||||
|
||||
const {
|
||||
resolveRuntimeArtifactLayout,
|
||||
} = require('../../gsd-core/bin/lib/runtime-artifact-layout.cjs');
|
||||
@@ -27,6 +28,12 @@ const {
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', '..', 'bin', 'install.js');
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
|
||||
// #3145: class-norm timeout (bounds the previously-unbounded spawnSync in
|
||||
// runMinimalInstall) — not a per-suite value. See helpers/timeouts.cjs for
|
||||
// the justification; every one of install-shared.cjs's 37+ importers
|
||||
// inherits this value, so it must stay generous rather than tight.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./timeouts.cjs');
|
||||
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'build-hooks.js');
|
||||
const HOOKS_DIST = path.join(__dirname, '..', '..', 'hooks', 'dist');
|
||||
|
||||
@@ -560,12 +567,24 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS
|
||||
configDir = runtime === 'cline' ? root : path.join(root, LOCAL_DIR_NAME[runtime]);
|
||||
}
|
||||
args.push(...extraArgs);
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd, encoding: 'utf8',
|
||||
const result = runNode(args, {
|
||||
cwd,
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root, ...extraEnv }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited with status ${result.status} for ${runtime} --${scope}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
// Kept as a hand-rolled assert (rather than throwIfFailed from
|
||||
// git-fixture.cjs) so the embedded stdout+stderr survives verbatim —
|
||||
// that is the whole diagnostic value of this message for a failing
|
||||
// install, and throwIfFailed's message only carries a trimmed stderr.
|
||||
// `result.exitCode` (never `result.status` — the seam has no such key)
|
||||
// is `null` for a non-EXITED outcome (TIMED_OUT/KILLED/BUFFER_OVERFLOW/
|
||||
// SPAWN_FAILED), so `outcome`/`timedOut`/`signal` are folded into the
|
||||
// message too: a bare "expected null to equal 0" would not tell anyone
|
||||
// the installer never actually exited.
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited with status ${result.exitCode} (outcome=${result.outcome}` +
|
||||
`${result.timedOut ? ', timedOut=true' : ''}${result.signal ? `, signal=${result.signal}` : ''}) ` +
|
||||
`for ${runtime} --${scope}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
const manifestPath = path.join(configDir, MANIFEST_NAME);
|
||||
const manifest = fs.existsSync(manifestPath)
|
||||
? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
|
||||
63
tests/helpers/timeouts.cjs
Normal file
63
tests/helpers/timeouts.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared CLASS-NORM subprocess timeouts for the test suite (#3145 pre-PR
|
||||
* review finding).
|
||||
*
|
||||
* These four values are not per-suite fixture bindings — each is a fact
|
||||
* about how long a CLASS of subprocess call takes, derived from observed
|
||||
* bench behavior. Before this module existed, all four were hand-copied
|
||||
* across dozens of files with the same justifying comment restated each
|
||||
* time (52 copies across this wave's diff alone), so the norm could drift
|
||||
* silently the next time it moved — as `INSTALL_TIMEOUT_MS` already did
|
||||
* once, from 60000 to 120000, after a real bench `ETIMEDOUT`. Import from
|
||||
* here instead of re-declaring.
|
||||
*
|
||||
* This module is for the shared norms ONLY. A call site that genuinely
|
||||
* differs from its class (e.g. a real `tsc` compile in
|
||||
* tests/ensure-runtime-build.test.cjs, or a `regen:derived` run in
|
||||
* tests/fragment-single-edit-propagation.install.test.cjs) keeps its own
|
||||
* local constant with its own justifying comment — do not force those
|
||||
* sites onto a shared value that doesn't describe them.
|
||||
*/
|
||||
|
||||
const { DEFAULT_GIT_TIMEOUT_MS } = require('./git-fixture.cjs');
|
||||
|
||||
/**
|
||||
* A single short CLI query or `node -e` probe against a temp fixture —
|
||||
* e.g. reading back a version string or a small piece of emitted state.
|
||||
* 15000ms is well over any observed duration for that class of call.
|
||||
*/
|
||||
const PROBE_TIMEOUT_MS = 15000;
|
||||
|
||||
/**
|
||||
* Git plumbing (rev-parse, branch, log, ...) against a small mkdtemp
|
||||
* fixture repo. Re-exports `tests/helpers/git-fixture.cjs`'s
|
||||
* `DEFAULT_GIT_TIMEOUT_MS` rather than restating the literal, so the two
|
||||
* can never disagree.
|
||||
*/
|
||||
const GIT_TIMEOUT_MS = DEFAULT_GIT_TIMEOUT_MS;
|
||||
|
||||
/**
|
||||
* Hooks bundling via `scripts/build-hooks.js` (not a full project build —
|
||||
* see per-site comments for sites that run a heavier build and therefore
|
||||
* keep a larger local value). 30000ms is well over any observed duration
|
||||
* for a hooks-only bundle pass.
|
||||
*/
|
||||
const BUILD_TIMEOUT_MS = 30000;
|
||||
|
||||
/**
|
||||
* A full `bin/install.js` run. Idle runs measure 13-30s; a load-tested
|
||||
* bench recorded a real `spawnSync ETIMEDOUT` at a 60000ms cap
|
||||
* (tests/install.test.cjs:5505-5513) while another lane passed the SAME
|
||||
* commit in 12.7s — 60000 is too tight for this class of spawn under
|
||||
* load. 120000ms is the load-tested norm.
|
||||
*/
|
||||
const INSTALL_TIMEOUT_MS = 120000;
|
||||
|
||||
module.exports = {
|
||||
PROBE_TIMEOUT_MS,
|
||||
GIT_TIMEOUT_MS,
|
||||
BUILD_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
};
|
||||
@@ -20,10 +20,12 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
||||
const isWindows = process.platform === 'win32';
|
||||
// 15000: a single bash hook script under test, not an install or a build.
|
||||
const HOOK_TIMEOUT_MS = 15000;
|
||||
|
||||
// Ensure the running node binary is on PATH so bash hooks can call `node`
|
||||
// (Claude Code shell sessions do not have `node` on PATH).
|
||||
@@ -33,8 +35,16 @@ const hookEnv = {
|
||||
};
|
||||
|
||||
// Wrapper that always injects hookEnv so bash hooks can find `node`.
|
||||
// Preserves the legacy spawnSync-shaped return (`status`, `stdout`, `stderr`,
|
||||
// `signal`) that every call site in this file asserts against.
|
||||
function spawnHook(hookPath, options) {
|
||||
return spawnSync('bash', [hookPath], { ...options, env: hookEnv });
|
||||
const r = runHook(hookPath, [], {
|
||||
...options,
|
||||
interpreter: 'bash',
|
||||
env: hookEnv,
|
||||
timeoutMs: HOOK_TIMEOUT_MS,
|
||||
});
|
||||
return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr, signal: r.signal };
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -28,7 +28,9 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync, execFileSync } = require('node:child_process');
|
||||
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
@@ -115,10 +117,9 @@ describe('install-profiles: MINIMAL_SKILL_ALLOWLIST', () => {
|
||||
|
||||
describe('install: --help profile counts match PROFILES (#834)', () => {
|
||||
function helpText() {
|
||||
return execFileSync(process.execPath, [INSTALL_SCRIPT, '--help'], {
|
||||
encoding: 'utf8',
|
||||
env: installerEnv(),
|
||||
});
|
||||
const r = runNode([INSTALL_SCRIPT, '--help'], { env: installerEnv(), timeoutMs: 15000 });
|
||||
throwIfFailed(r, `node ${INSTALL_SCRIPT} --help`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
test('core line advertises PROFILES.core.length main-loop skills', () => {
|
||||
@@ -442,10 +443,9 @@ describe('install: manifest records mode for both profiles', () => {
|
||||
function manifestModeAfterInstall(extraArgs) {
|
||||
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-manifest-mode-'));
|
||||
try {
|
||||
spawnSync(
|
||||
process.execPath,
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', targetDir, ...extraArgs],
|
||||
{ encoding: 'utf8', env: installerEnv() },
|
||||
{ env: installerEnv(), timeoutMs: 120000 },
|
||||
);
|
||||
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
||||
if (!fs.existsSync(manifestPath)) return { mode: '<no manifest>', skillCount: 0, agentCount: 0 };
|
||||
@@ -496,10 +496,9 @@ describe('install-minimal-backcompat: --minimal and --profile=core produce same
|
||||
function installAndGetManifest(extraArgs) {
|
||||
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-backcompat-'));
|
||||
try {
|
||||
spawnSync(
|
||||
process.execPath,
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', targetDir, ...extraArgs],
|
||||
{ encoding: 'utf8', env: installerEnv() },
|
||||
{ env: installerEnv(), timeoutMs: 120000 },
|
||||
);
|
||||
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
||||
if (!fs.existsSync(manifestPath)) return { mode: null, skillCount: 0, profileMarker: null };
|
||||
@@ -573,10 +572,9 @@ describe('install: Codex full → minimal downgrade cleans stale agent state', (
|
||||
// unsandboxed spawn here would write gsd-* skill dirs into the developer's
|
||||
// real $HOME/.agents/skills. This test only asserts on agents/ and
|
||||
// config.toml (both under targetDir), so the sandbox has no effect on intent.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--codex', '--global', '--config-dir', targetDir, '--minimal'],
|
||||
{ encoding: 'utf8', env: installerEnv({ HOME: targetDir, USERPROFILE: targetDir }) },
|
||||
{ env: installerEnv({ HOME: targetDir, USERPROFILE: targetDir }), timeoutMs: 120000 },
|
||||
);
|
||||
assert.ok(result.stdout || result.stderr);
|
||||
|
||||
@@ -611,10 +609,9 @@ describe('install: Claude full → minimal downgrade removes stale agents', () =
|
||||
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), 'stale\n');
|
||||
fs.writeFileSync(path.join(agentsDir, 'my-custom-agent.md'), 'user owns this\n');
|
||||
|
||||
spawnSync(
|
||||
process.execPath,
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', targetDir, '--minimal'],
|
||||
{ encoding: 'utf8', env: installerEnv() },
|
||||
{ env: installerEnv(), timeoutMs: 120000 },
|
||||
);
|
||||
|
||||
const remaining = fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir) : [];
|
||||
@@ -630,8 +627,11 @@ describe('install: Claude full → minimal downgrade removes stale agents', () =
|
||||
|
||||
// ─── Section 13: Hooks copy, manifest, uninstall settings cleanup ─────────────
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: SECTION13_BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: SECTION13_BUILD_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
@@ -701,13 +701,12 @@ describe('#1821/#2305: ZCode receives no dead hook files; Kilo/OpenCode/Claude k
|
||||
function installAndCollect(runtime, opts = {}) {
|
||||
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-1821-${runtime}-`));
|
||||
try {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, `--${runtime}`, '--global', '--config-dir', targetDir],
|
||||
{ encoding: 'utf8', env: installerEnv() },
|
||||
{ env: installerEnv(), timeoutMs: 120000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited with status ${result.status} for --${runtime} --global\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited with status ${result.exitCode} for --${runtime} --global\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
// Collect results while targetDir still exists — cleanup() below removes it.
|
||||
const pluginRelPath = opts.pluginRelPath || path.join('plugins', 'gsd-core.js');
|
||||
// #3023: the shared hooks bundle's staged directory name is per-runtime
|
||||
@@ -1238,16 +1237,20 @@ const { test, describe, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** Idempotently ensure hooks/dist contains built .js files. */
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1677,7 +1680,8 @@ const { test, describe, before, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
@@ -1694,13 +1698,16 @@ const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Idempotently ensure hooks/dist contains built .js files.
|
||||
* Runs build-hooks.js only when the directory is absent or empty of .js files.
|
||||
*/
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2989,16 +2996,20 @@ const { test, describe, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** Idempotently ensure hooks/dist contains built .js files. */
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3428,7 +3439,8 @@ const { test, describe, before, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
@@ -3445,13 +3457,16 @@ const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Idempotently ensure hooks/dist contains built .js files.
|
||||
* Runs build-hooks.js only when the directory is absent or empty of .js files.
|
||||
*/
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4097,12 +4112,16 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const SH_HOOKS = [
|
||||
'gsd-session-state.sh',
|
||||
'gsd-validate-commit.sh',
|
||||
@@ -4112,10 +4131,7 @@ const SH_HOOKS = [
|
||||
// ─── Ensure hooks/dist/ is populated before any install test ────────────────
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -4138,14 +4154,16 @@ function runInstaller(configDir) {
|
||||
// --no-sdk: this test covers hook deployment only; skip SDK build to avoid
|
||||
// flakiness and keep the test fast (SDK install path has dedicated coverage
|
||||
// in install-smoke.yml).
|
||||
execFileSync(process.execPath, [INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: configDir,
|
||||
},
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'], {
|
||||
timeoutMs: 120000,
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: configDir,
|
||||
},
|
||||
}),
|
||||
`node ${INSTALL_SCRIPT} --claude --global --yes --no-sdk`,
|
||||
);
|
||||
return path.join(configDir, 'hooks');
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
@@ -45,6 +46,8 @@ const {
|
||||
} = require('../gsd-core/bin/lib/install-engine.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const HOOKS_SRC = path.join(__dirname, '..', 'hooks');
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
@@ -83,16 +86,15 @@ describe('#2429 regression: Codex local skills stay project-scoped', () => {
|
||||
delete env.GSD_TEST_MODE;
|
||||
delete env.CODEX_HOME;
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--codex', '--local', '--profile=core'],
|
||||
{ cwd: projectDir, encoding: 'utf8', env, timeout: 60_000 },
|
||||
{ cwd: projectDir, env, timeoutMs: 60_000 },
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
result.exitCode,
|
||||
0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`,
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(projectDir, '.codex', 'skills', 'gsd-help', 'SKILL.md')),
|
||||
@@ -156,14 +158,13 @@ describe('Defect #2 regression (Qwen, #3664): --qwen --profile=core writes skill
|
||||
const root = createTempDir('gsd-qwen-reg2-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--qwen', '--global', '--config-dir', root, '--profile=core'],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const qwenSkillsDir = path.join(root, 'skills');
|
||||
assert.ok(fs.existsSync(qwenSkillsDir));
|
||||
@@ -189,14 +190,13 @@ describe('Defect #2 regression (Hermes, #3664): --hermes --profile=core writes s
|
||||
const root = createTempDir('gsd-hermes-reg2-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--hermes', '--global', '--config-dir', root, '--profile=core'],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const hermesSkillsGsd = path.join(root, 'skills', 'gsd');
|
||||
assert.ok(fs.existsSync(hermesSkillsGsd));
|
||||
@@ -227,14 +227,13 @@ describe('M1 (#2973, #947): --hermes --global --profile=core migrates dev-prefer
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my hermes prefs\n');
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--hermes', '--global', '--config-dir', root, '--profile=core'],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
// #947: Hermes uses prefix='gsd-' so dev-preferences lands at gsd-dev-preferences/ (not dev-preferences/)
|
||||
const skillFile = path.join(root, 'skills', 'gsd', 'gsd-dev-preferences', 'SKILL.md');
|
||||
@@ -257,14 +256,13 @@ describe('M2 (#2973): --qwen --global --profile=core migrates dev-preferences
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my qwen prefs\n');
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--qwen', '--global', '--config-dir', root, '--profile=core'],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const skillFile = path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillFile),
|
||||
@@ -285,14 +283,13 @@ describe('M3 (#2973): --claude --global --profile=core migrates dev-preferences
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my claude prefs\n');
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root, '--profile=core'],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const skillFile = path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillFile),
|
||||
@@ -649,14 +646,13 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
const root = createTempDir('gsd-claude-perm-install-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
assert.ok(fs.existsSync(settingsPath), 'settings.json must exist after claude install');
|
||||
@@ -679,14 +675,13 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
const root = createTempDir('gsd-antigravity-perm-install-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--antigravity', '--global', '--config-dir', root],
|
||||
{ encoding: 'utf8', env: { ...process.env, HOME: root, USERPROFILE: root } },
|
||||
{ env: { ...process.env, HOME: root, USERPROFILE: root }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(result.exitCode, 0,
|
||||
`installer exited ${result.exitCode}\n${result.stdout}\n${result.stderr}`);
|
||||
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
// If settings.json doesn't exist, permissions are definitely not written — pass.
|
||||
@@ -702,19 +697,19 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
const root = createTempDir('gsd-claude-perm-idempotent-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const spawnOpts = {
|
||||
encoding: 'utf8',
|
||||
const runOpts = {
|
||||
env: { ...process.env, HOME: root, USERPROFILE: root },
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
};
|
||||
const args = [INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root];
|
||||
|
||||
// First install
|
||||
const r1 = spawnSync(process.execPath, args, spawnOpts);
|
||||
assert.strictEqual(r1.status, 0, `first install failed: ${r1.stderr}`);
|
||||
const r1 = runNode(args, runOpts);
|
||||
assert.strictEqual(r1.exitCode, 0, `first install failed: ${r1.stderr}`);
|
||||
|
||||
// Second install (reinstall)
|
||||
const r2 = spawnSync(process.execPath, args, spawnOpts);
|
||||
assert.strictEqual(r2.status, 0, `reinstall failed: ${r2.stderr}`);
|
||||
const r2 = runNode(args, runOpts);
|
||||
assert.strictEqual(r2.exitCode, 0, `reinstall failed: ${r2.stderr}`);
|
||||
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(root, 'settings.json'), 'utf8'));
|
||||
for (const entry of GSD_CLAUDE_ALLOW_PERMISSIONS) {
|
||||
@@ -733,18 +728,17 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
const root = createTempDir('gsd-claude-perm-uninstall-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const spawnOpts = {
|
||||
encoding: 'utf8',
|
||||
const runOpts = {
|
||||
env: { ...process.env, HOME: root, USERPROFILE: root },
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
// Install first
|
||||
const r1 = spawnSync(
|
||||
process.execPath,
|
||||
const r1 = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root],
|
||||
spawnOpts,
|
||||
runOpts,
|
||||
);
|
||||
assert.strictEqual(r1.status, 0, `install failed: ${r1.stderr}`);
|
||||
assert.strictEqual(r1.exitCode, 0, `install failed: ${r1.stderr}`);
|
||||
|
||||
// Verify permissions were written
|
||||
const settingsPath = path.join(root, 'settings.json');
|
||||
@@ -758,12 +752,11 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(afterInstall, null, 2) + '\n');
|
||||
|
||||
// Uninstall
|
||||
const r2 = spawnSync(
|
||||
process.execPath,
|
||||
const r2 = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root, '--uninstall'],
|
||||
spawnOpts,
|
||||
runOpts,
|
||||
);
|
||||
assert.strictEqual(r2.status, 0, `uninstall failed: ${r2.stderr}`);
|
||||
assert.strictEqual(r2.exitCode, 0, `uninstall failed: ${r2.stderr}`);
|
||||
|
||||
const afterUninstall = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const allow = afterUninstall.permissions?.allow ?? [];
|
||||
@@ -788,18 +781,17 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
const root = createTempDir('gsd-claude-perm-uninstall-legacy-');
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const spawnOpts = {
|
||||
encoding: 'utf8',
|
||||
const runOpts = {
|
||||
env: { ...process.env, HOME: root, USERPROFILE: root },
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
// Install first (writes the current Edit(...) forms).
|
||||
const r1 = spawnSync(
|
||||
process.execPath,
|
||||
const r1 = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root],
|
||||
spawnOpts,
|
||||
runOpts,
|
||||
);
|
||||
assert.strictEqual(r1.status, 0, `install failed: ${r1.stderr}`);
|
||||
assert.strictEqual(r1.exitCode, 0, `install failed: ${r1.stderr}`);
|
||||
|
||||
// Simulate a pre-fix install that still carries the stale Write(...)
|
||||
// forms alongside the current Edit(...) forms and a user entry.
|
||||
@@ -809,12 +801,11 @@ describe('mergeClaudePermissions (#768): end-to-end install writes permissions t
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
// Uninstall
|
||||
const r2 = spawnSync(
|
||||
process.execPath,
|
||||
const r2 = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root, '--uninstall'],
|
||||
spawnOpts,
|
||||
runOpts,
|
||||
);
|
||||
assert.strictEqual(r2.status, 0, `uninstall failed: ${r2.stderr}`);
|
||||
assert.strictEqual(r2.exitCode, 0, `uninstall failed: ${r2.stderr}`);
|
||||
|
||||
const afterUninstall = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const allow = afterUninstall.permissions?.allow ?? [];
|
||||
@@ -1113,18 +1104,21 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before any install test ─────────────────
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -1148,11 +1142,13 @@ function runInstaller(configDir) {
|
||||
delete env.GSD_TEST_MODE;
|
||||
// --no-sdk: this test covers user-artifact preservation only; skip SDK
|
||||
// build (covered by install-smoke.yml) to keep the test deterministic.
|
||||
execFileSync(process.execPath, [INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'],
|
||||
{ env, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
),
|
||||
`node ${INSTALL_SCRIPT} --claude --global --yes --no-sdk`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Test 1: USER-PROFILE.md is preserved across re-install ─────────────────
|
||||
|
||||
@@ -2592,17 +2592,19 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const DRIFT_LINT = path.join(ROOT, 'scripts', 'lint-shell-command-projection-drift.cjs');
|
||||
|
||||
function runLint(targetFile) {
|
||||
return spawnSync(process.execPath, [DRIFT_LINT, targetFile], {
|
||||
const result = runNode([DRIFT_LINT, targetFile], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
return result;
|
||||
}
|
||||
|
||||
// (The buildWindowsShimTriple parity test was removed with the gsd-sdk shim,
|
||||
|
||||
@@ -30,8 +30,12 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { createTempDir, createTempProject, cleanup, parseFrontmatter } = require('./helpers.cjs');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const {
|
||||
getDirName,
|
||||
getConfigDirFromHome,
|
||||
@@ -930,8 +934,7 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)',
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const cliPath = path.join(claudeDir, 'scripts', 'changeset', 'cli.cjs');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const result = spawnSync(process.execPath, [cliPath, '--help'], { encoding: 'utf8' });
|
||||
const result = runNode([cliPath, '--help'], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
// --help exits with code 1 (usage shown), but must NOT throw a MODULE_NOT_FOUND error
|
||||
assert.ok(
|
||||
!result.stderr.includes('MODULE_NOT_FOUND'),
|
||||
@@ -952,11 +955,9 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)',
|
||||
// Use the CHANGELOG.md that was installed into gsd-core/ (installed by the installer)
|
||||
const changelogPath = path.join(claudeDir, 'gsd-core', 'CHANGELOG.md');
|
||||
assert.ok(fs.existsSync(changelogPath), 'CHANGELOG.md must be installed under gsd-core/');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[cliPath, 'extract', '--from', '0.0.0', '--to', '9999.0.0', '--changelog', changelogPath, '--json'],
|
||||
{ encoding: 'utf8' },
|
||||
{ timeoutMs: PROBE_TIMEOUT_MS },
|
||||
);
|
||||
// extract must NOT throw a MODULE_NOT_FOUND or Cannot find module error
|
||||
assert.ok(
|
||||
@@ -966,8 +967,8 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)',
|
||||
// extract exit code 0 (found entries) or 2 (no entries in range) are both valid;
|
||||
// any other exit code is an error
|
||||
assert.ok(
|
||||
result.status === 0 || result.status === 2,
|
||||
`installed cli.cjs extract must exit 0 or 2; got ${result.status}; stderr=${result.stderr}`,
|
||||
result.exitCode === 0 || result.exitCode === 2,
|
||||
`installed cli.cjs extract must exit 0 or 2; got ${result.exitCode}; stderr=${result.stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3503,7 +3504,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
@@ -3511,18 +3513,26 @@ const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
const PATCHES_DIR_NAME = 'gsd-local-patches';
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
function runInstaller(configDir) {
|
||||
const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
const r = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'],
|
||||
{ encoding: 'utf-8', stdio: 'pipe', env }
|
||||
{ env, timeoutMs: INSTALL_TIMEOUT_MS }
|
||||
);
|
||||
throwIfFailed(r, `node ${INSTALL_SCRIPT} --claude --global --yes --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
// ─── Test 1: writeManifest must NOT record USER-PROFILE.md ────────────────────
|
||||
@@ -4632,13 +4642,17 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
// With --test-concurrency=4, other install tests (bug-1834, bug-1924) run
|
||||
// build-hooks.js concurrently. That script creates hooks/dist/ empty first,
|
||||
@@ -4646,10 +4660,8 @@ const { cleanup } = require('./helpers.cjs');
|
||||
// install() fails with "directory is empty" → process.exit(1).
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── #1736 + #1367: local install deploys commands in flat gsd-<cmd>.md layout ───
|
||||
@@ -4762,19 +4774,21 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install, finishInstall } = require(INSTALL_SRC);
|
||||
const { cleanup, captureConsole } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── #2248: local install must NOT write statusLine to repo settings.json ────
|
||||
@@ -4921,19 +4935,21 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install, finishInstall } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── Helper: run both install phases ─────────────────────────────────────────
|
||||
@@ -5465,7 +5481,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
@@ -5473,6 +5490,12 @@ const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Ensure hooks/dist is populated before any suite that reads it.
|
||||
* hooks/dist/ is gitignored and only produced by `npm run build:hooks`.
|
||||
@@ -5482,7 +5505,8 @@ const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
*/
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
const r = runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5497,22 +5521,21 @@ function ensureHooksDist() {
|
||||
function runInstall(cwd, args) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
execFileSync(process.execPath, [INSTALL_PATH, ...args], {
|
||||
// 120s, not 60s. A full install copies and converts the whole shipped
|
||||
// payload (117 workflows, 100 references, 34 agents, ~71 skills) and
|
||||
// measures 13-30s on an idle runner — under 2x headroom at the old cap.
|
||||
// On a loaded bench that margin is not enough: the Cursor suite's before
|
||||
// hook died with `spawnSync ETIMEDOUT` on the node24 lane while the node22
|
||||
// lane passed the SAME commit in 12.7s, cancelling three child tests as
|
||||
// collateral. The cap also shrinks in real terms every time a file joins
|
||||
// the payload. Matches the 120s already used for the heavy install case
|
||||
// below. Aligned with the other runInstall helper in this file.
|
||||
const r = runNode([INSTALL_PATH, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
// 120s, not 60s. A full install copies and converts the whole shipped
|
||||
// payload (117 workflows, 100 references, 34 agents, ~71 skills) and
|
||||
// measures 13-30s on an idle runner — under 2x headroom at the old cap.
|
||||
// On a loaded bench that margin is not enough: the Cursor suite's before
|
||||
// hook died with `spawnSync ETIMEDOUT` on the node24 lane while the node22
|
||||
// lane passed the SAME commit in 12.7s, cancelling three child tests as
|
||||
// collateral. The cap also shrinks in real terms every time a file joins
|
||||
// the payload. Matches the 120s already used for the heavy install case
|
||||
// below. Aligned with the other runInstall helper in this file.
|
||||
timeout: 120000,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7527,7 +7550,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
@@ -7535,18 +7559,26 @@ const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
const PATCHES_DIR_NAME = 'gsd-local-patches';
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
function runInstaller(configDir) {
|
||||
const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
||||
delete env.GSD_TEST_MODE;
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
const r = runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'],
|
||||
{ encoding: 'utf-8', stdio: 'pipe', env }
|
||||
{ env, timeoutMs: INSTALL_TIMEOUT_MS }
|
||||
);
|
||||
throwIfFailed(r, `node ${INSTALL_SCRIPT} --claude --global --yes --no-sdk`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
// ─── Test 1: writeManifest must NOT record USER-PROFILE.md ────────────────────
|
||||
@@ -8656,13 +8688,17 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
// With --test-concurrency=4, other install tests (bug-1834, bug-1924) run
|
||||
// build-hooks.js concurrently. That script creates hooks/dist/ empty first,
|
||||
@@ -8670,10 +8706,8 @@ const { cleanup } = require('./helpers.cjs');
|
||||
// install() fails with "directory is empty" → process.exit(1).
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── #1736 + #1367: local install deploys commands in flat gsd-<cmd>.md layout ───
|
||||
@@ -8786,19 +8820,21 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install, finishInstall } = require(INSTALL_SRC);
|
||||
const { cleanup, captureConsole } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── #2248: local install must NOT write statusLine to repo settings.json ────
|
||||
@@ -8945,19 +8981,21 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install, finishInstall } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
// ─── Helper: run both install phases ─────────────────────────────────────────
|
||||
@@ -9489,7 +9527,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
@@ -9497,6 +9536,12 @@ const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Ensure hooks/dist is populated before any suite that reads it.
|
||||
* hooks/dist/ is gitignored and only produced by `npm run build:hooks`.
|
||||
@@ -9506,7 +9551,8 @@ const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
*/
|
||||
function ensureHooksDist() {
|
||||
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
const r = runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_HOOKS_SCRIPT}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9521,22 +9567,21 @@ function ensureHooksDist() {
|
||||
function runInstall(cwd, args) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
execFileSync(process.execPath, [INSTALL_PATH, ...args], {
|
||||
// 120s, not 60s. A full install copies and converts the whole shipped
|
||||
// payload (117 workflows, 100 references, 34 agents, ~71 skills) and
|
||||
// measures 13-30s on an idle runner — under 2x headroom at the old cap.
|
||||
// On a loaded bench that margin is not enough: the Cursor suite's before
|
||||
// hook died with `spawnSync ETIMEDOUT` on the node24 lane while the node22
|
||||
// lane passed the SAME commit in 12.7s, cancelling three child tests as
|
||||
// collateral. The cap also shrinks in real terms every time a file joins
|
||||
// the payload. Matches the 120s already used for the heavy install case
|
||||
// below. Aligned with the other runInstall helper in this file.
|
||||
const r = runNode([INSTALL_PATH, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
// 120s, not 60s. A full install copies and converts the whole shipped
|
||||
// payload (117 workflows, 100 references, 34 agents, ~71 skills) and
|
||||
// measures 13-30s on an idle runner — under 2x headroom at the old cap.
|
||||
// On a loaded bench that margin is not enough: the Cursor suite's before
|
||||
// hook died with `spawnSync ETIMEDOUT` on the node24 lane while the node22
|
||||
// lane passed the SAME commit in 12.7s, cancelling three child tests as
|
||||
// collateral. The cap also shrinks in real terms every time a file joins
|
||||
// the payload. Matches the 120s already used for the heavy install case
|
||||
// below. Aligned with the other runInstall helper in this file.
|
||||
timeout: 120000,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9918,7 +9963,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
@@ -9929,6 +9975,12 @@ const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
// leaves hooks/dist empty and install.js hard-fails "directory is empty" (#1926).
|
||||
const BUILD_HOOKS = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9940,12 +9992,12 @@ const BUILD_HOOKS = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
||||
function runClaudeLocalInstall(cwd) {
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
execFileSync(process.execPath, [INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
|
||||
const r = runNode([INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${INSTALL_PATH} --claude --local --no-sdk`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -9958,11 +10010,11 @@ describe('bug #1367 — Claude local install uses flat gsd-<cmd>.md command layo
|
||||
before(() => {
|
||||
// #1926: build hooks/dist/ so the installer's verifyInstalled(hooks) doesn't hit an
|
||||
// empty directory. Self-contained — no dependency on the lane having pre-built hooks.
|
||||
execFileSync(process.execPath, [BUILD_HOOKS], {
|
||||
const r = runNode([BUILD_HOOKS], {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeoutMs: BUILD_HOOKS_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `node ${BUILD_HOOKS}`);
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1367-'));
|
||||
runClaudeLocalInstall(tmpDir);
|
||||
});
|
||||
@@ -10100,9 +10152,13 @@ const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const os = require('node:os');
|
||||
|
||||
// A single short CLI query (install.js --skills-root <runtime>) — no full
|
||||
// install or build involved.
|
||||
const SKILLS_ROOT_PROBE_TIMEOUT_MS = 15000;
|
||||
|
||||
const INSTALL_JS = path.join(__dirname, '../bin/install.js');
|
||||
const WORKFLOW = path.join(__dirname, '../gsd-core/workflows/sync-skills.md');
|
||||
const COMMAND = path.join(__dirname, '../commands/gsd/sync-skills.md');
|
||||
@@ -10126,9 +10182,9 @@ describe('install.js --skills-root', () => {
|
||||
|
||||
for (const { runtime, expected } of CASES) {
|
||||
test(`resolves correct skills root for ${runtime}`, () => {
|
||||
const result = spawnSync(process.execPath, [INSTALL_JS, '--skills-root', runtime], {
|
||||
encoding: 'utf-8',
|
||||
const result = runNode([INSTALL_JS, '--skills-root', runtime], {
|
||||
env: { ...process.env, GSD_TEST_MODE: undefined }, // ensure not in test mode
|
||||
timeoutMs: SKILLS_ROOT_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
// Strip trailing newline
|
||||
const actual = result.stdout.trim();
|
||||
@@ -10137,15 +10193,15 @@ describe('install.js --skills-root', () => {
|
||||
}
|
||||
|
||||
test('exits non-zero when runtime arg is missing', () => {
|
||||
const result = spawnSync(process.execPath, [INSTALL_JS, '--skills-root'], {
|
||||
encoding: 'utf-8',
|
||||
const result = runNode([INSTALL_JS, '--skills-root'], {
|
||||
timeoutMs: SKILLS_ROOT_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Should exit with error when runtime arg is missing');
|
||||
assert.notStrictEqual(result.exitCode, 0, 'Should exit with error when runtime arg is missing');
|
||||
});
|
||||
|
||||
test('returns a path ending in /skills', () => {
|
||||
const result = spawnSync(process.execPath, [INSTALL_JS, '--skills-root', 'windsurf'], {
|
||||
encoding: 'utf-8',
|
||||
const result = runNode([INSTALL_JS, '--skills-root', 'windsurf'], {
|
||||
timeoutMs: SKILLS_ROOT_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
assert.ok(result.stdout.trim().endsWith('skills'), 'Skills root must end in /skills');
|
||||
});
|
||||
|
||||
@@ -11,16 +11,20 @@ process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const installModule = require('../bin/install.js');
|
||||
const pkg = require('../package.json');
|
||||
const { install } = installModule;
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const installScript = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const SUPPORTED_RUNTIMES = installModule.allRuntimes;
|
||||
const RUNTIME_INSTALL_CONTRACTS = {
|
||||
@@ -190,8 +194,7 @@ function runInstallerCli(runtime, targetDir, options = {}) {
|
||||
env.HOME = path.join(path.dirname(targetDir), 'home');
|
||||
env.USERPROFILE = env.HOME;
|
||||
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
return runNode(
|
||||
[
|
||||
installScript,
|
||||
`--${runtime}`,
|
||||
@@ -202,8 +205,8 @@ function runInstallerCli(runtime, targetDir, options = {}) {
|
||||
'--no-sdk',
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -583,7 +586,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
|
||||
const result = runInstallerCli(runtime, targetDir, { minimal: false });
|
||||
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
assert.equal(result.exitCode, 0, result.stderr || result.stdout);
|
||||
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
||||
assert.match(output, /Installing for /);
|
||||
assert.match(output, /Installer migrations/);
|
||||
@@ -614,7 +617,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
|
||||
const result = runInstallerCli(runtime, targetDir);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
assert.equal(result.exitCode, 0, result.stderr || result.stdout);
|
||||
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
||||
assert.match(output, /Installer migrations/);
|
||||
assert.match(output, /removed\s+hooks\/statusline\.js/);
|
||||
@@ -633,7 +636,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
|
||||
const result = runInstallerCli(runtime, targetDir);
|
||||
|
||||
assert.notEqual(result.status, 0, 'install should fail before materialization');
|
||||
assert.notEqual(result.exitCode, 0, 'install should fail before materialization');
|
||||
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
||||
assert.match(output, /Installer migrations/);
|
||||
assert.match(output, /blocked\s+gsd-core\/gsd-retired-tool\.cjs/);
|
||||
|
||||
@@ -2082,7 +2082,8 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const installModule = require('../bin/install.js');
|
||||
const { readInstallState } = require('../gsd-core/bin/lib/installer-migrations.cjs');
|
||||
@@ -2090,6 +2091,11 @@ const { install, parseTomlToObject, reconcileCodexHooksJsonEvent } = installModu
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist');
|
||||
const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything, even
|
||||
// run in beforeEach on a fresh worktree. See tests/helpers/timeouts.cjs for
|
||||
// the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
function withCodexHome(codexHome, fn) {
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
@@ -2147,7 +2153,10 @@ describe('#3357 — Codex install removes legacy GSD hooks.json entries', { conc
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) {
|
||||
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
||||
throwIfFailed(
|
||||
runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_HOOKS_SCRIPT}`,
|
||||
);
|
||||
}
|
||||
tmpRoot = createTempDir('gsd-3357-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
|
||||
@@ -20,7 +20,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync, execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
@@ -32,6 +33,12 @@ const {
|
||||
|
||||
const PKG_VERSION = require('../package.json').version;
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const {
|
||||
BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS,
|
||||
INSTALL_TIMEOUT_MS,
|
||||
} = require('./helpers/timeouts.cjs');
|
||||
|
||||
// The four-file hook set the Codex surface must deliver together (#2695).
|
||||
const CODEX_HOOK_FILES = [
|
||||
'gsd-check-update.js',
|
||||
@@ -42,7 +49,8 @@ const CODEX_HOOK_FILES = [
|
||||
|
||||
// Build hooks/dist before any install runs (the installer copies from there).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS });
|
||||
throwIfFailed(r, `node ${BUILD_SCRIPT}`);
|
||||
});
|
||||
|
||||
function hooksDirOf(configDir) {
|
||||
@@ -62,10 +70,9 @@ function runCodexInstall({ profile, preseed }) {
|
||||
// Sandbox HOME/USERPROFILE to configDir: Codex's skills-kind `home: ".agents"`
|
||||
// override resolves via os.homedir(); sandboxing keeps the spawn self-contained
|
||||
// (mirrors tests/install-minimal-hooks.test.cjs Codex downgrade test).
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--codex', '--global', '--config-dir', configDir, `--profile=${profile}`],
|
||||
{ encoding: 'utf8', env: installerEnv({ HOME: configDir, USERPROFILE: configDir }) },
|
||||
{ env: installerEnv({ HOME: configDir, USERPROFILE: configDir }), timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
return { configDir, result };
|
||||
}
|
||||
@@ -212,10 +219,9 @@ describe('#2695: re-running the installer is idempotent for the four-file set',
|
||||
const first = runCodexInstall({ profile: 'full' });
|
||||
t.after(() => cleanup(first.configDir));
|
||||
// Second run into the SAME config dir.
|
||||
const result2 = spawnSync(
|
||||
process.execPath,
|
||||
const result2 = runNode(
|
||||
[INSTALL_SCRIPT, '--codex', '--global', '--config-dir', first.configDir, '--profile=full'],
|
||||
{ encoding: 'utf8', env: installerEnv({ HOME: first.configDir, USERPROFILE: first.configDir }) },
|
||||
{ env: installerEnv({ HOME: first.configDir, USERPROFILE: first.configDir }), timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
assert.ok(result2.stdout || result2.stderr);
|
||||
|
||||
|
||||
@@ -31,11 +31,14 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
|
||||
// #3145: class-norm timeouts, not per-suite values — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS, INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const {
|
||||
install,
|
||||
uninstall,
|
||||
@@ -68,8 +71,8 @@ describe('#787 Cline pure helpers', () => {
|
||||
try {
|
||||
const p = path.join(tmp, 'PreToolUse');
|
||||
fs.writeFileSync(p, script);
|
||||
const res = spawnSync(process.execPath, ['--check', p], { encoding: 'utf8' });
|
||||
assert.equal(res.status, 0, `node --check failed: ${res.stderr}`);
|
||||
const res = runNode(['--check', p], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
assert.equal(res.exitCode, 0, `node --check failed: ${res.stderr}`);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
@@ -80,11 +83,11 @@ describe('#787 Cline pure helpers', () => {
|
||||
try {
|
||||
const p = path.join(tmp, 'PreToolUse');
|
||||
fs.writeFileSync(p, buildClinePreToolUseHook());
|
||||
const res = spawnSync(process.execPath, [p], {
|
||||
const res = runNode([p], {
|
||||
input: JSON.stringify({ toolName: 'read_file', toolInput: { path: 'src/index.ts' } }),
|
||||
encoding: 'utf8',
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(res.status, 0);
|
||||
assert.equal(res.exitCode, 0);
|
||||
const out = JSON.parse(res.stdout);
|
||||
assert.equal(out.cancel, false);
|
||||
} finally {
|
||||
@@ -97,11 +100,11 @@ describe('#787 Cline pure helpers', () => {
|
||||
try {
|
||||
const p = path.join(tmp, 'PreToolUse');
|
||||
fs.writeFileSync(p, buildClinePreToolUseHook());
|
||||
const res = spawnSync(process.execPath, [p], {
|
||||
const res = runNode([p], {
|
||||
input: JSON.stringify({ toolName: 'write_to_file', toolInput: { path: '.planning/ROADMAP.md', content: 'x' } }),
|
||||
encoding: 'utf8',
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(res.status, 0);
|
||||
assert.equal(res.exitCode, 0);
|
||||
const out = JSON.parse(res.stdout);
|
||||
assert.equal(out.cancel, true);
|
||||
assert.match(out.errorMessage, /\.planning/);
|
||||
@@ -115,14 +118,14 @@ describe('#787 Cline pure helpers', () => {
|
||||
try {
|
||||
const p = path.join(tmp, 'PreToolUse');
|
||||
fs.writeFileSync(p, buildClinePreToolUseHook());
|
||||
const res = spawnSync(process.execPath, [p], {
|
||||
const res = runNode([p], {
|
||||
input: JSON.stringify({
|
||||
toolName: 'write_to_file',
|
||||
toolInput: { path: 'docs/guide.md', content: 'Edit your .planning/ROADMAP.md via /gsd commands.' },
|
||||
}),
|
||||
encoding: 'utf8',
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(res.status, 0);
|
||||
assert.equal(res.exitCode, 0);
|
||||
assert.equal(JSON.parse(res.stdout).cancel, false, 'content mentioning .planning must not trigger a cancel');
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
@@ -134,8 +137,8 @@ describe('#787 Cline pure helpers', () => {
|
||||
try {
|
||||
const p = path.join(tmp, 'PreToolUse');
|
||||
fs.writeFileSync(p, buildClinePreToolUseHook());
|
||||
const res = spawnSync(process.execPath, [p], { input: 'not json{', encoding: 'utf8' });
|
||||
assert.equal(res.status, 0);
|
||||
const res = runNode([p], { input: 'not json{', timeoutMs: PROBE_TIMEOUT_MS });
|
||||
assert.equal(res.exitCode, 0);
|
||||
assert.equal(JSON.parse(res.stdout).cancel, false);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
@@ -263,10 +266,9 @@ describe('#787 Cline global install — ~/.agents/AGENTS.md', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-787-cline-global-'));
|
||||
const env = { ...process.env, HOME: root, USERPROFILE: root };
|
||||
delete env.GSD_TEST_MODE;
|
||||
const res = spawnSync(
|
||||
process.execPath,
|
||||
const res = runNode(
|
||||
[INSTALL_SCRIPT, '--cline', '--global', '--config-dir', path.join(root, '.cline')],
|
||||
{ cwd: root, encoding: 'utf8', env },
|
||||
{ cwd: root, env, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
);
|
||||
return { root, res };
|
||||
}
|
||||
@@ -274,7 +276,7 @@ describe('#787 Cline global install — ~/.agents/AGENTS.md', () => {
|
||||
test('writes ~/.agents/AGENTS.md with a GSD marker block', () => {
|
||||
const { root, res } = runGlobalClineInstall();
|
||||
try {
|
||||
assert.equal(res.status, 0, `installer failed: ${res.stderr}`);
|
||||
assert.equal(res.exitCode, 0, `installer failed: ${res.stderr}`);
|
||||
const agents = path.join(root, '.agents', 'AGENTS.md');
|
||||
assert.ok(fs.existsSync(agents), '~/.agents/AGENTS.md must exist after a global Cline install');
|
||||
const content = fs.readFileSync(agents, 'utf8');
|
||||
|
||||
@@ -32,6 +32,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode, runGit } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
@@ -309,9 +310,13 @@ test('capabilities/kilo/capability.json extendedHookEvents is exactly [] (hooksS
|
||||
// hooks/dist is gitignored and built; the scoped CI lane does not run
|
||||
// build:hooks, so a real install there would stage no hooks/ dir. Build it
|
||||
// idempotently (mirrors golden-install-parity + install-minimal-hooks).
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
before(() => {
|
||||
const build = spawnSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf8' });
|
||||
assert.equal(build.status, 0, `build:hooks failed: ${build.stderr}`);
|
||||
const build = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS });
|
||||
assert.equal(build.exitCode, 0, `build:hooks failed: ${build.stderr}`);
|
||||
});
|
||||
|
||||
// The three PreToolUse guards the plugin spawns that ship today. When a new
|
||||
@@ -373,8 +378,8 @@ test('kilo: a disallowed write through the REAL installed tree is rejected by th
|
||||
const mainRepo = path.join(scratch, 'main');
|
||||
fs.mkdirSync(mainRepo, { recursive: true });
|
||||
const git = (args, cwd) => {
|
||||
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd, encoding: 'utf8' });
|
||||
assert.equal(r.status, 0, `git ${args.join(' ')} failed: ${r.stderr}`);
|
||||
const r = runGit(['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd });
|
||||
assert.equal(r.exitCode, 0, `git ${args.join(' ')} failed: ${r.stderr}`);
|
||||
return r;
|
||||
};
|
||||
git(['init', '-q'], mainRepo);
|
||||
|
||||
@@ -174,7 +174,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { installerEnv } = require('./helpers/install-shared.cjs');
|
||||
@@ -185,6 +185,8 @@ const ROOT = path.join(__dirname, '..');
|
||||
const KIMI_CFG = path.join(os.tmpdir(), 'gsd-kimi-config-test').replace(/\\/g, '/');
|
||||
const XDG_HOME = path.join(os.tmpdir(), 'gsd-xdg-home-test');
|
||||
const INSTALL_SCRIPT = path.join(ROOT, 'bin', 'install.js');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const {
|
||||
getGlobalConfigDir,
|
||||
@@ -306,18 +308,17 @@ describe('Kimi runtime homes', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-existing-agents-home-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmpHome, '.agents', 'skills'), { recursive: true });
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--kimi', '--global', '--no-sdk'],
|
||||
{
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
result.exitCode,
|
||||
0,
|
||||
`expected --kimi --global to reuse existing ~/.agents/skills\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
||||
);
|
||||
@@ -362,18 +363,17 @@ describe('Kimi local install guard', () => {
|
||||
const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-local-project-'));
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-local-home-'));
|
||||
try {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--kimi', '--local', '--no-sdk'],
|
||||
{
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
result.exitCode,
|
||||
0,
|
||||
`expected --kimi --local guard to no-op successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
||||
);
|
||||
@@ -396,18 +396,17 @@ describe('Kimi local install guard', () => {
|
||||
const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-config-'));
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-home-'));
|
||||
try {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'],
|
||||
{
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
result.exitCode,
|
||||
0,
|
||||
`expected --kimi --global to install Kimi skills successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
||||
);
|
||||
@@ -480,13 +479,13 @@ describe('Kimi local install guard', () => {
|
||||
|
||||
try {
|
||||
const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'];
|
||||
const first = spawnSync(process.execPath, installArgs, {
|
||||
const first = runNode(installArgs, {
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
assert.strictEqual(
|
||||
first.status,
|
||||
first.exitCode,
|
||||
0,
|
||||
`first install failed\nstdout: ${first.stdout}\nstderr: ${first.stderr}`,
|
||||
);
|
||||
@@ -496,13 +495,13 @@ describe('Kimi local install guard', () => {
|
||||
fs.appendFileSync(skillFile, '\nUSER LOCAL KIMI SKILL EDIT\n');
|
||||
fs.appendFileSync(agentPrompt, '\nUSER LOCAL KIMI AGENT EDIT\n');
|
||||
|
||||
const second = spawnSync(process.execPath, installArgs, {
|
||||
const second = runNode(installArgs, {
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
assert.strictEqual(
|
||||
second.status,
|
||||
second.exitCode,
|
||||
0,
|
||||
`second install failed\nstdout: ${second.stdout}\nstderr: ${second.stderr}`,
|
||||
);
|
||||
@@ -534,13 +533,13 @@ describe('Kimi local install guard', () => {
|
||||
|
||||
try {
|
||||
const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'];
|
||||
const installResult = spawnSync(process.execPath, installArgs, {
|
||||
const installResult = runNode(installArgs, {
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
assert.strictEqual(
|
||||
installResult.status,
|
||||
installResult.exitCode,
|
||||
0,
|
||||
`install failed\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`,
|
||||
);
|
||||
@@ -554,17 +553,16 @@ describe('Kimi local install guard', () => {
|
||||
fs.writeFileSync(foreignRootAgent, 'version: 1\n', 'utf8');
|
||||
fs.writeFileSync(foreignSubagent, 'version: 1\n', 'utf8');
|
||||
|
||||
const uninstallResult = spawnSync(
|
||||
process.execPath,
|
||||
const uninstallResult = runNode(
|
||||
[INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--uninstall'],
|
||||
{
|
||||
cwd: tmpProject,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
assert.strictEqual(
|
||||
uninstallResult.status,
|
||||
uninstallResult.exitCode,
|
||||
0,
|
||||
`uninstall failed\nstdout: ${uninstallResult.stdout}\nstderr: ${uninstallResult.stderr}`,
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { runMinimalInstall, INSTALL_SCRIPT, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup, createTempDir, toPosixPath } = require('./helpers.cjs');
|
||||
@@ -142,12 +142,13 @@ test('kimi --global: reinstalling is idempotent — the GSD [[hooks]] block is n
|
||||
// Reinstall over the SAME root/config (runMinimalInstall always mkdtemps a
|
||||
// fresh root, so the reinstall is driven directly against this test's root
|
||||
// exactly the way runMinimalInstall drives its own install internally).
|
||||
const reinstall = spawnSync(process.execPath, [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', root], {
|
||||
cwd: process.cwd(), encoding: 'utf8',
|
||||
const reinstall = runNode([INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', root], {
|
||||
cwd: process.cwd(),
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
assert.strictEqual(reinstall.status, 0,
|
||||
`reinstall exited with status ${reinstall.status}\nstdout: ${reinstall.stdout}\nstderr: ${reinstall.stderr}`);
|
||||
assert.strictEqual(reinstall.exitCode, 0,
|
||||
`reinstall exited with status ${reinstall.exitCode}\nstdout: ${reinstall.stdout}\nstderr: ${reinstall.stderr}`);
|
||||
const second = fs.readFileSync(tomlPath, 'utf8');
|
||||
assert.equal(beginMarkers(second), 1, 'reinstall must not duplicate the BEGIN marker');
|
||||
assert.equal(endMarkers(second), 1, 'reinstall must not duplicate the END marker');
|
||||
|
||||
@@ -775,18 +775,17 @@ describe('renderLoopHooks', () => {
|
||||
|
||||
// ─── 9. End-to-end cmdLoopRenderHooks (via gsd-tools subprocess) ─────────────
|
||||
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const GSD_TOOLS = path.join(ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
test('loop render-hooks plan:pre returns JSON envelope with ui-phase step active', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpProjectDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(envelope.point, 'plan:pre');
|
||||
assert.ok(Array.isArray(envelope.activeHooks));
|
||||
@@ -799,12 +798,11 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
|
||||
// FIX 4: schema-default activation — no config.json in project → ui-phase step active by default
|
||||
test('loop render-hooks plan:pre with no config.json → ui-phase step active by schema default', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpEmptyProjectDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
const uiStep = envelope.activeHooks.find(h => h.capId === 'ui' && h.kind === 'step');
|
||||
assert.ok(
|
||||
@@ -815,8 +813,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
});
|
||||
|
||||
test('loop render-hooks plan:pre with ui capability disabled in surface → ui hooks absent', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[
|
||||
GSD_TOOLS,
|
||||
'loop',
|
||||
@@ -827,9 +824,9 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
'--config-dir',
|
||||
tmpUiDisabledConfigDir,
|
||||
],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
const uiHooks = envelope.activeHooks.filter(h => h.capId === 'ui');
|
||||
assert.deepStrictEqual(
|
||||
@@ -841,12 +838,11 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
|
||||
// FIX 4: explicit false in config.json overrides schema default
|
||||
test('loop render-hooks plan:pre with ui_phase=false in config.json → ui-phase step absent', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpFalseConfigProjectDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
const envelope = JSON.parse(result.stdout.trim());
|
||||
const uiStep = envelope.activeHooks.find(h => h.capId === 'ui' && h.kind === 'step');
|
||||
assert.strictEqual(
|
||||
@@ -857,12 +853,11 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
|
||||
});
|
||||
|
||||
test('loop render-hooks invalid-point exits non-zero', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:mid', '--cwd', tmpProjectDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.notStrictEqual(result.status, 0, 'Expected non-zero exit for invalid point');
|
||||
assert.notStrictEqual(result.exitCode, 0, 'Expected non-zero exit for invalid point');
|
||||
assert.match(result.stderr, /plan:mid|Invalid loop point/);
|
||||
});
|
||||
});
|
||||
@@ -901,53 +896,48 @@ describe('--active-cap flag (loop render-hooks)', () => {
|
||||
});
|
||||
|
||||
test('--active-cap tdd with tdd_mode=true → stdout trimmed === "true", exit 0', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOnDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.stdout.trim(), 'true', 'Expected stdout "true" when tdd_mode=true');
|
||||
});
|
||||
|
||||
test('--active-cap tdd with tdd_mode=false → stdout trimmed === "false", exit 0', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOffDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.stdout.trim(), 'false', 'Expected stdout "false" when tdd_mode=false');
|
||||
});
|
||||
|
||||
test('--active-cap <nonexistent-cap> → stdout trimmed === "false", exit 0', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'no-such-capability-xyz', '--cwd', tddOffDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0 for unknown capId. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0 for unknown capId. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.stdout.trim(), 'false', 'Expected stdout "false" for unknown capId');
|
||||
});
|
||||
|
||||
test('--active-cap with no value → non-zero exit and error message', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', '--cwd', tddOffDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.notStrictEqual(result.status, 0, 'Expected non-zero exit when --active-cap has no value');
|
||||
assert.notStrictEqual(result.exitCode, 0, 'Expected non-zero exit when --active-cap has no value');
|
||||
assert.match(result.stderr, /active-cap/i, 'Expected error message referencing --active-cap');
|
||||
});
|
||||
|
||||
test('--active-cap output is exactly "true" or "false" (no JSON envelope, clean for shell capture)', () => {
|
||||
// The entire stdout must be just "true" or "false" + newline — no envelope object
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOnDir],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
{ cwd: ROOT, timeoutMs: 15000 },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
// Must be exactly "true" or "false" — not a JSON object/envelope
|
||||
const trimmed = result.stdout.trim();
|
||||
assert.ok(
|
||||
@@ -1072,12 +1062,11 @@ describe('ADR-1244 D2: load-failed capability gates fail OPEN with a loud warnin
|
||||
}
|
||||
|
||||
function renderHooks(overlayHome, point, extraArgs = []) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
const result = runNode(
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', point, '--cwd', overlayHome, ...extraArgs],
|
||||
{ cwd: ROOT, encoding: 'utf8', env: { ...process.env, GSD_HOME: overlayHome } },
|
||||
{ cwd: ROOT, timeoutMs: 15000, env: { ...process.env, GSD_HOME: overlayHome } },
|
||||
);
|
||||
assert.strictEqual(result.status, 0, `Expected exit 0 at ${point}. stderr: ` + (result.stderr || ''));
|
||||
assert.strictEqual(result.exitCode, 0, `Expected exit 0 at ${point}. stderr: ` + (result.stderr || ''));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,13 +126,18 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
||||
const _CHECK_UPDATE_FILE = path.join(HOOKS_DIR, 'gsd-check-update.js');
|
||||
const WORKER_FILE = path.join(HOOKS_DIR, 'gsd-check-update-worker.js');
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { INSTALL_TIMEOUT_MS, PROBE_TIMEOUT_MS, BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const SH_HOOKS = [
|
||||
'gsd-phase-boundary.sh',
|
||||
@@ -143,10 +148,10 @@ const SH_HOOKS = [
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -163,11 +168,13 @@ function cleanup(dir) {
|
||||
function runInstaller(configDir) {
|
||||
// --no-sdk: this test covers .sh hook version stamping only; skip SDK
|
||||
// build (covered by install-smoke.yml).
|
||||
execFileSync(process.execPath, [INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir },
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'],
|
||||
{ env: { ...process.env, CLAUDE_CONFIG_DIR: configDir }, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
),
|
||||
`node ${INSTALL_SCRIPT} --claude --global --yes --no-sdk`,
|
||||
);
|
||||
return path.join(configDir, 'hooks');
|
||||
}
|
||||
|
||||
@@ -377,8 +384,9 @@ describe('bug #2136 part 4: installed .sh hooks contain stamped concrete version
|
||||
process.stdout.write(JSON.stringify(staleHooks));
|
||||
`;
|
||||
|
||||
const result = execFileSync(process.execPath, ['-e', checkScript], { encoding: 'utf8' });
|
||||
const staleHooks = JSON.parse(result);
|
||||
const probeResult = runNode(['-e', checkScript], { timeoutMs: PROBE_TIMEOUT_MS });
|
||||
throwIfFailed(probeResult, 'node -e <bug-2136 stale-hook probe script>');
|
||||
const staleHooks = JSON.parse(probeResult.stdout);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
staleHooks,
|
||||
@@ -428,23 +436,28 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const TOOLS_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
// scripts/build-hooks.js copies pre-built hook files into hooks/dist and
|
||||
// syntax-checks them with vm — it does not compile/bundle anything. See
|
||||
// tests/helpers/timeouts.cjs for the class-norm justification.
|
||||
const { INSTALL_TIMEOUT_MS, PROBE_TIMEOUT_MS, BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const { HOOKS_TO_COPY } = require('../scripts/build-hooks.js');
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before any install test ────────────────
|
||||
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
|
||||
`node ${BUILD_SCRIPT}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -469,11 +482,13 @@ function runInstaller(configDir) {
|
||||
// must not skip the main() gate or the install is a no-op.
|
||||
const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
||||
delete env.GSD_TEST_MODE;
|
||||
execFileSync(process.execPath, [INSTALL_SCRIPT, '--claude', '--global', '--yes'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
});
|
||||
throwIfFailed(
|
||||
runNode(
|
||||
[INSTALL_SCRIPT, '--claude', '--global', '--yes'],
|
||||
{ env, timeoutMs: INSTALL_TIMEOUT_MS },
|
||||
),
|
||||
`node ${INSTALL_SCRIPT} --claude --global --yes`,
|
||||
);
|
||||
return configDir;
|
||||
}
|
||||
|
||||
@@ -481,12 +496,12 @@ function runInstaller(configDir) {
|
||||
* Run detect-custom-files and return parsed JSON output.
|
||||
*/
|
||||
function detectCustomFiles(configDir) {
|
||||
const result = execFileSync(process.execPath, [TOOLS_PATH, 'detect-custom-files', '--config-dir', configDir], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, GSD_SESSION_KEY: '' },
|
||||
});
|
||||
return JSON.parse(result.trim());
|
||||
const result = runNode(
|
||||
[TOOLS_PATH, 'detect-custom-files', '--config-dir', configDir],
|
||||
{ env: { ...process.env, GSD_SESSION_KEY: '' }, timeoutMs: PROBE_TIMEOUT_MS },
|
||||
);
|
||||
throwIfFailed(result, `node ${TOOLS_PATH} detect-custom-files --config-dir ${configDir}`);
|
||||
return JSON.parse(result.stdout.trim());
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -60,7 +60,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runMinimalInstall, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
@@ -71,6 +71,9 @@ const { buildCatalog, readResource, shouldCompose } = require('../gsd-core/bin/l
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const MARKER_TOKEN = 'gsd:section';
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/** Recursively collect `.md` file paths under `absDir`, relative to `REPO_ROOT`, POSIX-normalized. */
|
||||
function collectMarkdownFiles(absDir, out = []) {
|
||||
let entries;
|
||||
@@ -130,10 +133,10 @@ function installOverlay(overlayRoot, runtime, extraArgs = []) {
|
||||
root,
|
||||
...extraArgs,
|
||||
];
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
const result = runNode(args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
return { configDir: root, root, result };
|
||||
}
|
||||
@@ -242,7 +245,7 @@ describe('a marker-documenting non-workflow composes on neither surface (row 50)
|
||||
const dest = installOverlay(overlayRepo, 'claude');
|
||||
t.after(() => cleanup(dest.root));
|
||||
assert.equal(
|
||||
dest.result.status,
|
||||
dest.result.exitCode,
|
||||
0,
|
||||
`install must succeed: a non-workflow doc's marker-shaped line must never reach composeWorkflow\nstderr: ${dest.result.stderr}`,
|
||||
);
|
||||
@@ -291,7 +294,7 @@ describe('the parity gate is non-vacuous against a real installer regression (ro
|
||||
const dest = installOverlay(brokenPredicateRepo, 'claude');
|
||||
t.after(() => cleanup(dest.root));
|
||||
assert.equal(
|
||||
dest.result.status,
|
||||
dest.result.exitCode,
|
||||
0,
|
||||
`broken-predicate install must still succeed (composeWorkflow simply never runs)\nstderr: ${dest.result.stderr}`,
|
||||
);
|
||||
|
||||
@@ -31,8 +31,9 @@ const REPO_ROOT = path.join(__dirname, '..');
|
||||
// the time this guard was written (139 entries) — each future migration
|
||||
// wave lowers it as files are moved off the allowlist by adding real
|
||||
// timeouts; it must never grow back up. Lowered to 120 by the #3144 Wave-1
|
||||
// process-seam migration (19 files' unbounded spawns bounded).
|
||||
const BASELINE = 120;
|
||||
// process-seam migration (19 files' unbounded spawns bounded), then to 73
|
||||
// by the #3145 Wave-2 migration (47 files' unbounded spawns bounded).
|
||||
const BASELINE = 73;
|
||||
|
||||
function readAllowlist() {
|
||||
const raw = fs.readFileSync(ALLOWLIST_PATH, 'utf8');
|
||||
|
||||
@@ -207,6 +207,207 @@ describe('no-unbounded-spawn: A — detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ceiling escape (allow-spawn-timeout-ceiling) — #3145 matrix section A ──
|
||||
//
|
||||
// Rows A1-A13 of .gsd/phase/chore-3145-bound-installer-runtime/50-test-matrix.md.
|
||||
// (Distinct from the "A — detection" section above, which is #3143's matrix.)
|
||||
|
||||
describe('no-unbounded-spawn: ceiling escape (allow-spawn-timeout-ceiling)', () => {
|
||||
test('A1: exactly the ceiling is still clean without a marker', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [{ code: `spawnSync(c, a, { timeout: 600000 });`, filename: FILE }],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A2: just over the ceiling still reports without a marker', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `spawnSync(c, a, { timeout: 600001 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A3: a reasoned marker permits an over-ceiling bound', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: regen:derived runs a full build\n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A4: an empty reason is not an audit trail', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling:\n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A5: whitespace-only reason is rejected', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: \n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A6: the marker binds to its own call, not the file', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: reason for the first call\n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });\n` +
|
||||
`spawnSync(c, b, { timeout: 700000 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A7: an inert marker is not itself an error', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: reason\n` +
|
||||
`spawnSync(c, a, { timeout: 5000 });`,
|
||||
filename: FILE,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A8: the escape raises the ceiling, it never waives the bound', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: reason\n` +
|
||||
`spawnSync(c, a, {});`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'unboundedSpawn' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A9: marker is recognized above the call', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [
|
||||
{
|
||||
code:
|
||||
`// allow-spawn-timeout-ceiling: reason above the call\n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A10: marker is recognized inline at the timeout', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [
|
||||
{
|
||||
code:
|
||||
`spawnSync(c, a, {\n` +
|
||||
` // allow-spawn-timeout-ceiling: reason inline at the timeout\n` +
|
||||
` timeout: 900000,\n` +
|
||||
`});`,
|
||||
filename: FILE,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A11: only a real comment counts as a marker', () => {
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code:
|
||||
`const note = 'allow-spawn-timeout-ceiling: reason in a string';\n` +
|
||||
`spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A12: an unrelated marker does not apply', () => {
|
||||
// The marker text is built via concatenation, not a string literal, so
|
||||
// this file does not itself contain the contiguous exemption-directive
|
||||
// text (the no-source-grep marker, name split across the concat below) —
|
||||
// the refs linter that guards that directive can't tell fixture data
|
||||
// proving non-suppression from a real exemption, and a literal here
|
||||
// would be misread as an unreferenced one. Same idiom as GUARDED_RULE
|
||||
// in no-unbounded-spawn-allowlist.test.cjs.
|
||||
const UNRELATED_MARKER = '// ' + 'allow-test-rule' + ': unrelated escape\n';
|
||||
ruleTester.run('local/no-unbounded-spawn', rule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: UNRELATED_MARKER + `spawnSync(c, a, { timeout: 900000 });`,
|
||||
filename: FILE,
|
||||
errors: [{ messageId: 'timeoutTooLarge' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('A13: the real 900000 site passes with its marker', () => {
|
||||
const { ESLint } = require('eslint');
|
||||
const eslint = new ESLint({
|
||||
overrideConfigFile: true,
|
||||
overrideConfig: {
|
||||
languageOptions: { ecmaVersion: 2022, sourceType: 'commonjs' },
|
||||
plugins: { local: { rules: { 'no-unbounded-spawn': rule } } },
|
||||
rules: { 'local/no-unbounded-spawn': ['error', { allowlist: [] }] },
|
||||
},
|
||||
});
|
||||
return eslint
|
||||
.lintFiles(['tests/fragment-single-edit-propagation.install.test.cjs'])
|
||||
.then((results) => {
|
||||
const messages = results[0] ? results[0].messages : [];
|
||||
const tooLarge = messages.filter((m) => m.messageId === 'timeoutTooLarge');
|
||||
assert.deepEqual(tooLarge, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── B. the timeout value (Goodhart defenses) ──────────────────────────────
|
||||
|
||||
describe('no-unbounded-spawn: B — timeout value boundaries', () => {
|
||||
|
||||
@@ -33,13 +33,15 @@ const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const {
|
||||
runMinimalInstall,
|
||||
installerEnv,
|
||||
INSTALL_SCRIPT,
|
||||
} = require('./helpers/install-shared.cjs');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
resolveRuntimeArtifactLayout,
|
||||
@@ -61,11 +63,12 @@ function reinstallOpencode(root, scope = 'global') {
|
||||
args.push('--local');
|
||||
cwd = root;
|
||||
}
|
||||
return spawnSync(process.execPath, args, {
|
||||
const r = runNode(args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
||||
}
|
||||
|
||||
function gsdMdFiles(dir) {
|
||||
|
||||
@@ -381,15 +381,18 @@ test('plugin implements the full declared opencode extension-event surface (Clau
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('installer copies plugin as .js, records it in the manifest, and removes it on uninstall', (t) => {
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const installer = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const cfg = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-oc-install-')));
|
||||
t.after(() => cleanup(cfg));
|
||||
|
||||
const run = (args) =>
|
||||
spawnSync(process.execPath, [installer, '--opencode', '--global', '--config-dir', cfg, ...args], {
|
||||
encoding: 'utf8',
|
||||
const run = (args) => {
|
||||
const result = runNode([installer, '--opencode', '--global', '--config-dir', cfg, ...args], {
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
result.status = result.exitCode;
|
||||
return result;
|
||||
};
|
||||
|
||||
// Install
|
||||
const install = run([]);
|
||||
|
||||
@@ -4,7 +4,8 @@ const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
@@ -31,16 +32,20 @@ describe('.githooks/pre-commit alias drift guard', () => {
|
||||
|
||||
const marker = path.join(tmpDir, 'npm-called.txt');
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_OVERRIDE: mockGit,
|
||||
NPM_OVERRIDE: mockNpm,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runHook(HOOK_PATH, [], {
|
||||
interpreter: 'bash',
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_OVERRIDE: mockGit,
|
||||
NPM_OVERRIDE: mockNpm,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
timeoutMs: 15000,
|
||||
}),
|
||||
`bash ${HOOK_PATH}`,
|
||||
);
|
||||
|
||||
assert.ok(fs.existsSync(marker), 'expected npm run check:alias-drift to be invoked');
|
||||
});
|
||||
@@ -54,16 +59,20 @@ describe('.githooks/pre-commit alias drift guard', () => {
|
||||
|
||||
const marker = path.join(tmpDir, 'npm-called.txt');
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_OVERRIDE: mockGit,
|
||||
NPM_OVERRIDE: mockNpm,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
});
|
||||
throwIfFailed(
|
||||
runHook(HOOK_PATH, [], {
|
||||
interpreter: 'bash',
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_OVERRIDE: mockGit,
|
||||
NPM_OVERRIDE: mockNpm,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
timeoutMs: 15000,
|
||||
}),
|
||||
`bash ${HOOK_PATH}`,
|
||||
);
|
||||
|
||||
assert.ok(!fs.existsSync(marker), 'expected npm check to be skipped for unrelated staged files');
|
||||
});
|
||||
|
||||
@@ -4,12 +4,16 @@ const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const HOOK_PATH = path.join(ROOT, '.githooks', 'pre-push');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Write a mock bash script to a .sh file in tmpDir and return its absolute path.
|
||||
* The hook invokes it via GIT_OVERRIDE — bash executes the path directly via the
|
||||
@@ -47,7 +51,8 @@ exit 1
|
||||
`);
|
||||
|
||||
assert.throws(() => {
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
const r = runHook(HOOK_PATH, [], {
|
||||
interpreter: 'bash',
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -55,8 +60,9 @@ exit 1
|
||||
GSD_BLOCKED_AUTHOR_REGEX: '@example-corp\\.com$',
|
||||
},
|
||||
input: 'refs/heads/pr refs-local-sha refs/heads/pr refs-remote-sha\n',
|
||||
stdio: 'pipe',
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `bash ${HOOK_PATH}`);
|
||||
}, /Push blocked: commit author email matched local blocked regex/);
|
||||
});
|
||||
|
||||
@@ -78,7 +84,8 @@ fi
|
||||
exit 1
|
||||
`);
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
const r = runHook(HOOK_PATH, [], {
|
||||
interpreter: 'bash',
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -86,7 +93,8 @@ exit 1
|
||||
GSD_BLOCKED_AUTHOR_REGEX: '@example-corp\\.com$',
|
||||
},
|
||||
input: 'refs/heads/pr refs-local-sha refs/heads/pr refs-remote-sha\n',
|
||||
stdio: 'pipe',
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
});
|
||||
throwIfFailed(r, `bash ${HOOK_PATH}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (init/config/add/commit/branch/worktree/merge/checkout)
|
||||
// on a small mkdtemp fixture repo — far over any observed duration for that
|
||||
// class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// Lazy-loaded so tests can fail clearly when the export doesn't exist yet.
|
||||
function getPruneOrphanedWorktrees() {
|
||||
|
||||
@@ -22,10 +22,8 @@ const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const RELEASE_WORKFLOW = path.join(__dirname, '..', '.github', 'workflows', 'release.yml');
|
||||
|
||||
// 15000ms: git plumbing (config, add, commit, rev-parse, checkout, cherry-pick)
|
||||
// against a small mkdtemp fixture repo — gitOrThrow's own documented default,
|
||||
// reused here as the named constant for this file's calls.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── git helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const { test, describe, mock } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const { computeMigrationPlan, applyMigration } = require('../gsd-core/bin/lib/roadmap-upgrade.cjs');
|
||||
@@ -18,13 +18,13 @@ function makeGitignoredPlanningProject() {
|
||||
const dir = createTempDir('m3-rollback-');
|
||||
fs.writeFileSync(path.join(dir, '.gitignore'), '.planning/\n');
|
||||
fs.writeFileSync(path.join(dir, 'README.md'), '# tracked\n');
|
||||
const git = (c) => execSync(c, { cwd: dir, stdio: 'pipe' });
|
||||
git('git init');
|
||||
git('git config user.email t@t.t');
|
||||
git('git config user.name t');
|
||||
git('git config commit.gpgsign false');
|
||||
git('git add -A');
|
||||
git('git commit -m initial');
|
||||
const git = (argv) => gitOrThrow(argv, { cwd: dir });
|
||||
git(['init']);
|
||||
git(['config', 'user.email', 't@t.t']);
|
||||
git(['config', 'user.name', 't']);
|
||||
git(['config', 'commit.gpgsign', 'false']);
|
||||
git(['add', '-A']);
|
||||
git(['commit', '-m', 'initial']);
|
||||
|
||||
// .planning created AFTER the commit → untracked + gitignored.
|
||||
const planning = path.join(dir, '.planning');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,10 +38,8 @@ const {
|
||||
readVerificationStatus,
|
||||
} = require('../gsd-core/bin/lib/verification.cjs');
|
||||
|
||||
// 15000ms: git plumbing (init/config/add/commit) against a small mkdtemp
|
||||
// fixture repo, plus a `git --version` availability probe — gitOrThrow's own
|
||||
// documented default, reused here as this file's named constant.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -10,11 +10,8 @@ const { runGsdTools, createTempProject, createTempGitProject, cleanup } = requir
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
|
||||
/**
|
||||
* Bound for every git subprocess in this file: plumbing (init/config/add/
|
||||
* commit/rev-parse) against small fixture repos — well under this. #3144.
|
||||
*/
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
/**
|
||||
* Bound for the grep/sed availability probes and region-extraction calls in
|
||||
|
||||
@@ -44,8 +44,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { runHook: runHookSeam } = require('./helpers/process-seam.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
@@ -76,10 +76,10 @@ function runHook(scriptPath, payload, opts = {}) {
|
||||
}
|
||||
|
||||
function initGitRepo(dir) {
|
||||
execFileSync('git', ['init', '-q', '.'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.email', 'gsd-test@example.com'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.name', 'gsd-test'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '--allow-empty', '-q', '-m', 'init'], { cwd: dir, stdio: 'pipe' });
|
||||
gitOrThrow(['init', '-q', '.'], { cwd: dir });
|
||||
gitOrThrow(['config', 'user.email', 'gsd-test@example.com'], { cwd: dir });
|
||||
gitOrThrow(['config', 'user.name', 'gsd-test'], { cwd: dir });
|
||||
gitOrThrow(['commit', '--allow-empty', '-q', '-m', 'init'], { cwd: dir });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -49,7 +49,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { RUNTIME_META, runMinimalInstall, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
@@ -58,6 +58,8 @@ const { executionContextRefs } = require('../scripts/command-contract-helpers.cj
|
||||
const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md');
|
||||
const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL);
|
||||
// plan-phase.md was the original #2930 pilot but was reverted to unmarked
|
||||
@@ -88,11 +90,12 @@ function spawnGlobalInstall(installScript, runtime, extraArgs = []) {
|
||||
root,
|
||||
...extraArgs,
|
||||
];
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
const seamResult = runNode(args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
});
|
||||
const result = { status: seamResult.exitCode, stdout: seamResult.stdout, stderr: seamResult.stderr };
|
||||
return { result, configDir: root, root };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,8 @@ const path = require('path');
|
||||
const { runGsdTools, createTempProject, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { detectChildRepos } = require('../gsd-core/bin/lib/init.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (init/config/add/commit/worktree/clone) on a small
|
||||
// mkdtemp fixture repo — far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ─── detectChildRepos ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -24,16 +24,13 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { BUILD_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
|
||||
const { install, finishInstall } = require(INSTALL_SRC);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// 60000ms: matches the process seam's own default for a Node CLI run — this
|
||||
// is a real build step (scripts/build-hooks.js), not fixture plumbing.
|
||||
const BUILD_TIMEOUT_MS = 60000;
|
||||
|
||||
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
|
||||
before(() => {
|
||||
const r = runNode([BUILD_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS });
|
||||
|
||||
@@ -1503,10 +1503,9 @@ const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const EXECUTE_PHASE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'execute-phase.md');
|
||||
|
||||
// 15000ms: git plumbing (init/config/add/commit/worktree) against a small
|
||||
// mkdtemp fixture repo — gitOrThrow's own documented default; the guard
|
||||
// itself keeps its separately-justified 30000ms (see runGuard below).
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
// The guard itself keeps its separately-justified 30000ms (see runGuard below).
|
||||
const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract the cwd-drift guard bash block from execute-phase.md
|
||||
|
||||
@@ -52,6 +52,11 @@ const STALE_MTIME = new Date(0);
|
||||
/** The lock-owner PID written into every fixture; liveness is always injected. */
|
||||
const LOCK_OWNER_PID = '4242';
|
||||
|
||||
// #3145: deliberately double the GIT_TIMEOUT_MS class norm (see
|
||||
// helpers/timeouts.cjs) — each test here does real-git worktree/branch setup
|
||||
// AND a `.git/worktrees/<name>/` admin-directory mutation AND one or more
|
||||
// reapOrphanWorktrees invocations, more subprocess work per test than the
|
||||
// plain fixture-setup case the norm is sized for.
|
||||
const GIT_TIMEOUT_MS = 30000;
|
||||
|
||||
// ─── Path + git helpers ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user