The windows-test-parity ratchet greps test source for fs.rmSync-without-
maxRetries (and six other Windows-portability anti-patterns), failing when an
integer offender COUNT exceeds a frozen baseline (rmSync: 95). A count ratchet
is a Goodhart metric: fixing one offender and adding another keeps the count
constant, so a new defect slips through green. Replace it — and every other
count ratchet in the repo — with a layered, masking-proof design.
Behavioral seam test
- tests/helpers-cleanup.test.cjs proves helpers.cleanup() carries the Windows
EBUSY retry budget. cleanup() delegates retries to Node's fs.rmSync via
maxRetries (it owns no loop), so the test asserts the option contract
(recursive/force/maxRetries>0/retryDelay>0) + real-FS removal + the cwd-guard,
rather than a loop that does not exist. The EBUSY risk is now tested ONCE at
the helper, not approximated textually at every call site.
Write-time ESLint rule (AST-accurate, replaces the grep)
- eslint-rules/no-raw-rmsync-in-tests.cjs (error in tests/**/*.test.cjs) bans
raw fs.rmSync, steering to cleanup(). Catches member, computed (fs['rmSync']),
destructured and aliased forms; escape hatch is inline
`// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>` only.
- Migrated 336 raw fs.rmSync teardown calls across ~116 test files to cleanup().
~18 genuinely load-bearing sites (mid-test SUT/fault-injection removals,
error-swallowing or name-colliding local teardown helpers) keep the raw call
with an inline eslint-disable + reason.
Shared anti-ratchet primitive
- scripts/lib/allowlist-ratchet.cjs:
- assertWithinAllowlist: fails on NOVEL ids (new offender introduced) AND on
STALE ids (a known offender was fixed but not pruned) — identity, not count,
and a ratchet DOWN toward zero.
- assertTightCeiling: a size/length budget whose ceiling must stay within a
grace band of the high-water mark, so budgets may only tighten, never creep.
Ratchets converted onto the primitive
- windows-test-parity-guard.test.cjs: rmSync rule deleted (now ESLint-enforced);
the remaining six patterns moved from integer baselines to named-set
allowlists with ratchet-down.
- scripts/lint-test-file-count.{cjs,allowlist.json}: per-module integer counts →
named filename sets (closes the swap-a-file-keep-the-count blind spot); a
module dropping under cap now FAILS to force pruning its allowlist entry.
- enh-2790 skill-count `<= 63` → named skill allowlist (ratchets toward ~58).
Size budgets hardened (tighten-only)
- agent-size / workflow-size / feat-3039 help-tiered: ceilings lowered to the
current high-water mark and an assertTightCeiling anti-creep check added per
tier. Fixed external-contract limits (description ≤100 chars, agent ≤100 KB)
are intentionally left as-is — they are not grandfathered creeping budgets.
No user-facing behavior change (tests + tooling only); no USER_FACING_PREFIXES
touched, so no changeset fragment is required.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
198
eslint-rules/no-raw-rmsync-in-tests.cjs
Normal file
198
eslint-rules/no-raw-rmsync-in-tests.cjs
Normal file
@@ -0,0 +1,198 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* no-raw-rmsync-in-tests
|
||||
*
|
||||
* In *.test.cjs files, flag any call that invokes fs.rmSync (directly or via
|
||||
* destructuring/aliasing). Covers:
|
||||
*
|
||||
* (a) MemberExpression with identifier property "rmSync":
|
||||
* fs.rmSync(d, opts) nodeFs.rmSync(d, opts)
|
||||
*
|
||||
* (b) MemberExpression with computed string-literal property "rmSync":
|
||||
* fs['rmSync'](d, opts)
|
||||
*
|
||||
* (c) Bare Identifier whose name is known to be bound to an fs rmSync via:
|
||||
* const { rmSync } = require('fs'|'node:fs')
|
||||
* const alias = fs.rmSync (where fs is a require('fs') binding)
|
||||
* const alias = require('fs').rmSync
|
||||
*
|
||||
* The only escape hatch is the native inline disable comment:
|
||||
* // eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>
|
||||
* (ESLint handles that automatically — this rule does not implement it.)
|
||||
*
|
||||
* NOTE: The file-level `// allow-test-rule:` annotation does NOT apply to this
|
||||
* rule. That annotation is for no-source-grep only.
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow raw fs.rmSync() calls in test files; use helpers.cleanup() instead',
|
||||
category: 'Best Practices',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noRawRmSync:
|
||||
'Raw fs.rmSync() in a test. Use helpers.cleanup(dir) instead — it carries the Windows-EBUSY retry budget (maxRetries/retryDelay). To suppress a rare legit case, use `// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>`.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Only applies in test files
|
||||
if (!filename.endsWith('.test.cjs')) return {};
|
||||
|
||||
// --- Track fs-derived bindings ---
|
||||
// fsBindings: Set of local variable names bound to require('fs'|'node:fs')
|
||||
// rmSyncBindings: Set of local variable names bound to an fs.rmSync value
|
||||
const fsBindings = new Set();
|
||||
const rmSyncBindings = new Set();
|
||||
|
||||
/**
|
||||
* Returns true if `node` is a require('fs') or require('node:fs') call.
|
||||
*/
|
||||
function isFsRequire(node) {
|
||||
return (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'require' &&
|
||||
node.arguments.length === 1 &&
|
||||
node.arguments[0].type === 'Literal' &&
|
||||
(node.arguments[0].value === 'fs' ||
|
||||
node.arguments[0].value === 'node:fs')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `node` is a reference to a known fs binding (Identifier
|
||||
* whose name is in fsBindings).
|
||||
*/
|
||||
function isFsBinding(node) {
|
||||
return node.type === 'Identifier' && fsBindings.has(node.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `node` is an expression that resolves to fs.rmSync:
|
||||
* - fsBinding.rmSync (MemberExpression, identifier property)
|
||||
* - require('fs').rmSync
|
||||
*/
|
||||
function isFsRmSyncExpression(node) {
|
||||
if (node.type !== 'MemberExpression') return false;
|
||||
const prop = node.property;
|
||||
const isRmSyncProp =
|
||||
(!node.computed &&
|
||||
prop.type === 'Identifier' &&
|
||||
prop.name === 'rmSync') ||
|
||||
(node.computed &&
|
||||
prop.type === 'Literal' &&
|
||||
prop.value === 'rmSync');
|
||||
if (!isRmSyncProp) return false;
|
||||
return isFsBinding(node.object) || isFsRequire(node.object);
|
||||
}
|
||||
|
||||
return {
|
||||
// ── Track `const fs = require('fs')` ──────────────────────────────────
|
||||
VariableDeclaration(node) {
|
||||
for (const decl of node.declarations) {
|
||||
if (!decl.init) continue;
|
||||
|
||||
// const fs = require('fs')
|
||||
if (
|
||||
decl.id.type === 'Identifier' &&
|
||||
isFsRequire(decl.init)
|
||||
) {
|
||||
fsBindings.add(decl.id.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// const { rmSync } = require('fs')
|
||||
// const { rmSync: del } = require('fs')
|
||||
if (
|
||||
decl.id.type === 'ObjectPattern' &&
|
||||
isFsRequire(decl.init)
|
||||
) {
|
||||
for (const prop of decl.id.properties) {
|
||||
if (
|
||||
prop.type === 'Property' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'rmSync' &&
|
||||
prop.value.type === 'Identifier'
|
||||
) {
|
||||
rmSyncBindings.add(prop.value.name);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// const { rmSync } = fs (where fs is already a known binding)
|
||||
if (
|
||||
decl.id.type === 'ObjectPattern' &&
|
||||
isFsBinding(decl.init)
|
||||
) {
|
||||
for (const prop of decl.id.properties) {
|
||||
if (
|
||||
prop.type === 'Property' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'rmSync' &&
|
||||
prop.value.type === 'Identifier'
|
||||
) {
|
||||
rmSyncBindings.add(prop.value.name);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// const alias = fs.rmSync (or require('fs').rmSync)
|
||||
if (
|
||||
decl.id.type === 'Identifier' &&
|
||||
isFsRmSyncExpression(decl.init)
|
||||
) {
|
||||
rmSyncBindings.add(decl.id.name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── Flag rmSync calls ─────────────────────────────────────────────────
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
|
||||
// (a) obj.rmSync(...) — identifier property
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
!callee.computed &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'rmSync'
|
||||
) {
|
||||
context.report({ node, messageId: 'noRawRmSync' });
|
||||
return;
|
||||
}
|
||||
|
||||
// (b) obj['rmSync'](...) — computed string-literal property
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.computed &&
|
||||
callee.property.type === 'Literal' &&
|
||||
callee.property.value === 'rmSync'
|
||||
) {
|
||||
context.report({ node, messageId: 'noRawRmSync' });
|
||||
return;
|
||||
}
|
||||
|
||||
// (c) bare identifier known to be fs.rmSync
|
||||
if (
|
||||
callee.type === 'Identifier' &&
|
||||
rmSyncBindings.has(callee.name)
|
||||
) {
|
||||
context.report({ node, messageId: 'noRawRmSync' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = rule;
|
||||
@@ -8,16 +8,18 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Local plugin with three custom AST rules
|
||||
// Local plugin with custom AST rules
|
||||
import noSourceGrep from './eslint-rules/no-source-grep.cjs';
|
||||
import noMagicSleepInTests from './eslint-rules/no-magic-sleep-in-tests.cjs';
|
||||
import noElapsedAssertion from './eslint-rules/no-elapsed-assertion.cjs';
|
||||
import noRawRmsyncInTests from './eslint-rules/no-raw-rmsync-in-tests.cjs';
|
||||
|
||||
const localPlugin = {
|
||||
rules: {
|
||||
'no-source-grep': noSourceGrep,
|
||||
'no-magic-sleep-in-tests': noMagicSleepInTests,
|
||||
'no-elapsed-assertion': noElapsedAssertion,
|
||||
'no-raw-rmsync-in-tests': noRawRmsyncInTests,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -108,6 +110,8 @@ export default tseslint.config(
|
||||
// Timing anti-patterns — warn for now; flip to error after cleanup
|
||||
'local/no-magic-sleep-in-tests': 'warn',
|
||||
'local/no-elapsed-assertion': 'warn',
|
||||
// Ban raw fs.rmSync in tests — use helpers.cleanup() for Windows-EBUSY retry budget
|
||||
'local/no-raw-rmsync-in-tests': 'error',
|
||||
// Ban raw setTimeout sync + elapsed/duration-style assertions via no-restricted-syntax
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
|
||||
136
scripts/lib/allowlist-ratchet.cjs
Normal file
136
scripts/lib/allowlist-ratchet.cjs
Normal file
@@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @file allowlist-ratchet.cjs
|
||||
*
|
||||
* Reusable "better than a count ratchet" primitives for CI guards.
|
||||
*
|
||||
* ## Motivation (issue #597)
|
||||
*
|
||||
* A count ratchet (`assert(offenders.length <= N)`) has a masking blind spot:
|
||||
* fixing one offender and introducing a new one keeps the count constant, so a
|
||||
* novel defect slips through green. These helpers enforce on IDENTITY instead,
|
||||
* making every individual offender visible and requiring monotonic progress
|
||||
* toward zero.
|
||||
*
|
||||
* ## Design
|
||||
*
|
||||
* Both functions are pure (no I/O, no global state). The `fail` callback is
|
||||
* injected by the caller so the same logic can be used with `node:assert.fail`,
|
||||
* a custom throw, or a message-collector in unit tests.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Assert that `current` offenders are all within the known allowlist, and that
|
||||
* every entry in the allowlist still offends (forcing the allowlist to shrink
|
||||
* as defects are fixed).
|
||||
*
|
||||
* Fails when:
|
||||
* - Any id in `current` is NOT in `known` → novel offender introduced.
|
||||
* - Any id in `known` is NOT in `current` → stale allowlist entry must be
|
||||
* pruned so the guard ratchets toward zero (the ratchet-DOWN direction).
|
||||
*
|
||||
* ## Masking blind spot this prevents (issue #597)
|
||||
*
|
||||
* A count ratchet (`assert(count <= N)`) allows one offender to be silently
|
||||
* replaced by another while the count stays at N. By asserting on identity
|
||||
* instead, every new offender is caught by name, and every fixed offender
|
||||
* forces the allowlist to shrink.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.label - Human-readable name for the guard (used
|
||||
* in failure messages).
|
||||
* @param {Iterable<string>} opts.current - The offending ids found in the
|
||||
* current run.
|
||||
* @param {Iterable<string>} opts.known - The allowlisted ids (baseline).
|
||||
* @param {function(string): void} opts.fail - Callback invoked with a
|
||||
* descriptive message on any violation.
|
||||
* Pass `require('node:assert').fail`, a
|
||||
* custom thrower, or a collector. The
|
||||
* function is NOT imported here so callers
|
||||
* control the failure mode.
|
||||
* @param {string} [opts.pruneHint] - Optional hint appended to the stale-
|
||||
* entry failure message (e.g. the name of
|
||||
* the allowlist file to edit).
|
||||
* @returns {{ novel: string[], stale: string[] }} Sorted arrays of novel ids
|
||||
* (in current but not known) and stale ids (in known but not current).
|
||||
*/
|
||||
function assertWithinAllowlist({ label, current, known, fail, pruneHint }) {
|
||||
const currentSet = new Set(current);
|
||||
const knownSet = new Set(known);
|
||||
|
||||
const novel = [...currentSet].filter((id) => !knownSet.has(id)).sort();
|
||||
const stale = [...knownSet].filter((id) => !currentSet.has(id)).sort();
|
||||
|
||||
if (novel.length > 0) {
|
||||
const list = novel.map((id) => ` - ${id}`).join('\n');
|
||||
fail(
|
||||
`[${label}] ${novel.length} NEW offender(s) introduced — fix at the source; do not just add to the allowlist.\n${list}`
|
||||
);
|
||||
}
|
||||
|
||||
if (stale.length > 0) {
|
||||
const list = stale.map((id) => ` - ${id}`).join('\n');
|
||||
const hint = pruneHint ? `\n(${pruneHint})` : '';
|
||||
fail(
|
||||
`[${label}] ${stale.length} allowlisted id(s) no longer offend and MUST be pruned so the guard ratchets toward zero.${hint}\n${list}`
|
||||
);
|
||||
}
|
||||
|
||||
return { novel, stale };
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that an artifact's measured maximum stays within a declared ceiling,
|
||||
* and that the ceiling itself does not creep above the high-water mark (budgets
|
||||
* may only decrease, not increase over time).
|
||||
*
|
||||
* Fails when:
|
||||
* - `actualMax > ceiling` → regression: artifact exceeds budget.
|
||||
* - `ceiling - actualMax > grace` → ceiling sits too far above the measured
|
||||
* value; tighten it toward `actualMax`.
|
||||
*
|
||||
* ## Masking blind spot this prevents (issue #597)
|
||||
*
|
||||
* A plain `assert(size <= ceiling)` with a ceiling set generously high allows
|
||||
* the artifact to grow unchecked as long as it stays under the ceiling. The
|
||||
* `grace` band forces the ceiling to stay close to the high-water mark,
|
||||
* ensuring that any upward creep is immediately visible.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.label - Human-readable name for the guard (used
|
||||
* in failure messages).
|
||||
* @param {number} opts.actualMax - The measured value (e.g. bundle size in
|
||||
* bytes, line count).
|
||||
* @param {number} opts.ceiling - The declared budget ceiling.
|
||||
* @param {number} opts.grace - Maximum allowed slack (`ceiling -
|
||||
* actualMax`) before the ceiling is
|
||||
* considered too loose.
|
||||
* @param {function(string): void} opts.fail - Callback invoked with a
|
||||
* descriptive message on any violation.
|
||||
* @returns {{ ok: boolean, slack: number }} Whether both checks passed and the
|
||||
* current slack value.
|
||||
*/
|
||||
function assertTightCeiling({ label, actualMax, ceiling, grace, fail }) {
|
||||
const slack = ceiling - actualMax;
|
||||
let ok = true;
|
||||
|
||||
if (actualMax > ceiling) {
|
||||
ok = false;
|
||||
fail(
|
||||
`[${label}] Regression: artifact value ${actualMax} exceeds budget ceiling ${ceiling}. ` +
|
||||
`Raise the ceiling to at most ${actualMax} only if the increase is justified.`
|
||||
);
|
||||
} else if (slack > grace) {
|
||||
ok = false;
|
||||
fail(
|
||||
`[${label}] Ceiling ${ceiling} sits too far above the high-water mark ${actualMax} ` +
|
||||
`(slack ${slack} > grace ${grace}). Tighten the ceiling toward ${actualMax}. ` +
|
||||
`Budgets may only decrease.`
|
||||
);
|
||||
}
|
||||
|
||||
return { ok, slack };
|
||||
}
|
||||
|
||||
module.exports = { assertWithinAllowlist, assertTightCeiling };
|
||||
@@ -1,36 +1,131 @@
|
||||
{
|
||||
"_doc": "Baseline of modules currently exceeding the 2-test-file limit. Each entry locks in TODAY's count as the ceiling. Reductions are ratcheted automatically — when a cluster drops to ≤ 2, remove its entry. New entries require justification in PR description.",
|
||||
"_doc": "Baseline of modules currently exceeding the 2-test-file limit. Each entry locks in TODAY's exact test filenames as the allowlisted set (identity ratchet). Adding a NEW test file to a capped module fails (novel). Removing one requires pruning this list (stale, ratchet-down). When a cluster drops to ≤ 2, remove its entry entirely. New entries require justification in PR description.",
|
||||
"modules": {
|
||||
"phase": { "current": 5, "issue": "3788" },
|
||||
"worktree": { "current": 13, "issue": "TBD" },
|
||||
"milestone": { "current": 10, "issue": "TBD" },
|
||||
"roadmap": { "current": 9, "issue": "TBD" },
|
||||
"verify": { "current": 10, "issue": "3767" },
|
||||
"install": { "current": 9, "issue": "TBD" },
|
||||
"init": { "current": 8, "issue": "TBD" },
|
||||
"state": { "current": 11, "issue": "180" },
|
||||
"config": { "current": 8, "issue": "TBD" },
|
||||
"graphify": { "current": 7, "issue": "TBD" },
|
||||
"progress": { "current": 6, "issue": "14" },
|
||||
"cli": { "current": 5, "issue": "TBD" },
|
||||
"surface": { "current": 5, "issue": "TBD" },
|
||||
"commit": { "current": 4, "issue": "TBD" },
|
||||
"frontmatter": { "current": 4, "issue": "TBD" },
|
||||
"index": { "current": 5, "issue": "180" },
|
||||
"intel": { "current": 4, "issue": "TBD" },
|
||||
"mvp": { "current": 4, "issue": "TBD" },
|
||||
"install-profiles": { "current": 4, "issue": "TBD" },
|
||||
"audit": { "current": 4, "issue": "192" },
|
||||
"audit-open": { "current": 3, "issue": "TBD" },
|
||||
"config-schema": { "current": 3, "issue": "TBD" },
|
||||
"profile": { "current": 3, "issue": "TBD" },
|
||||
"prompt-budget": { "current": 3, "issue": "TBD" },
|
||||
"uat": { "current": 3, "issue": "TBD" },
|
||||
"validate": { "current": 3, "issue": "TBD" },
|
||||
"workstream": { "current": 3, "issue": "TBD" },
|
||||
"gsd-tools": { "current": 3, "issue": "TBD" },
|
||||
"runtime-artifact-layout":{ "current": 3, "issue": "TBD" },
|
||||
"security": { "current": 3, "issue": "TBD" },
|
||||
"gsd-sdk": { "current": 3, "issue": "TBD" }
|
||||
"audit": {
|
||||
"files": [
|
||||
"audit-fix-command.test.cjs",
|
||||
"bug-2659-audit-open-crash.test.cjs",
|
||||
"bug-2836-audit-open-summary-uat-drift.test.cjs",
|
||||
"bug-2911-audit-open-output-shape.test.cjs"
|
||||
],
|
||||
"issue": "192"
|
||||
},
|
||||
"config": {
|
||||
"files": [
|
||||
"bug-2943-config-get-context-window-default.test.cjs",
|
||||
"bug-321-config-defaults-clone-strategy.test.cjs",
|
||||
"bug-3227-config-set-model-overrides.test.cjs",
|
||||
"bug-442-config-dir-equals-in-path.test.cjs",
|
||||
"config-field-docs.test.cjs",
|
||||
"config-get-default.test.cjs",
|
||||
"config-schema.property.test.cjs",
|
||||
"config.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"graphify": {
|
||||
"files": [
|
||||
"graphify-auto-update.test.cjs",
|
||||
"graphify-query.test.cjs",
|
||||
"graphify-visualization.test.cjs",
|
||||
"graphify.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"intel": {
|
||||
"files": [
|
||||
"bug-2351-intel-kilo-layout.test.cjs",
|
||||
"bug-3290-intel-updater-layout-block.test.cjs",
|
||||
"intel.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"milestone": {
|
||||
"files": [
|
||||
"milestone-archive.test.cjs",
|
||||
"milestone-helper.test.cjs",
|
||||
"milestone-prefixed-convention.test.cjs",
|
||||
"milestone-summary.test.cjs",
|
||||
"milestone.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"phase": {
|
||||
"files": [
|
||||
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
|
||||
"phase-dependency-levels.test.cjs",
|
||||
"phase.test.cjs"
|
||||
],
|
||||
"issue": "597"
|
||||
},
|
||||
"roadmap": {
|
||||
"files": [
|
||||
"bug-2661-roadmap-sync-parallel.test.cjs",
|
||||
"bug-3128-roadmap-plan-count-slug-layout.test.cjs",
|
||||
"bug-3599-roadmap-get-phase-project-code-prefix.test.cjs",
|
||||
"enh-2447-roadmap-wave-deps.test.cjs",
|
||||
"roadmap-mode-field.test.cjs",
|
||||
"roadmap-phase-fallback.test.cjs",
|
||||
"roadmap.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"runtime-artifact-layout": {
|
||||
"files": [
|
||||
"runtime-artifact-layout-install-profiles.test.cjs",
|
||||
"runtime-artifact-layout-surface.test.cjs",
|
||||
"runtime-artifact-layout.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"security": {
|
||||
"files": [
|
||||
"security-prompt-injection.test.cjs",
|
||||
"security-scan.test.cjs",
|
||||
"security.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"state": {
|
||||
"files": [
|
||||
"bug-21-state-md-template-frontmatter.test.cjs",
|
||||
"bug-2630-state-frontmatter-milestone-switch.test.cjs",
|
||||
"bug-3127-state-begin-phase-idempotent.test.cjs",
|
||||
"bug-3242-state-update-progress-trample.test.cjs",
|
||||
"bug-3286-state-write-routing.test.cjs",
|
||||
"bug-3454-state-dollar-backreference-growth.test.cjs",
|
||||
"bug-397-state-preserve-executor-authored.test.cjs",
|
||||
"state-acquirestatelock-non-eexist.test.cjs",
|
||||
"state-prune.test.cjs",
|
||||
"state.test.cjs"
|
||||
],
|
||||
"issue": "180"
|
||||
},
|
||||
"verify": {
|
||||
"files": [
|
||||
"bug-2969-verify-reapply-patches.test.cjs",
|
||||
"bug-2994-verify-reapply-patches-installed-path.test.cjs",
|
||||
"bug-3381-verify-work-workstream.test.cjs",
|
||||
"bug-3657-verify-reapply-patches-pristine-drift.test.cjs",
|
||||
"verify-health.test.cjs",
|
||||
"verify-mvp-uat.test.cjs",
|
||||
"verify-test-quality.test.cjs",
|
||||
"verify-work-auto-transition.test.cjs",
|
||||
"verify.test.cjs"
|
||||
],
|
||||
"issue": "3767"
|
||||
},
|
||||
"install": {
|
||||
"files": [
|
||||
"bug-410-install-defaults-test-mode-guard.test.cjs",
|
||||
"install-minimal-hooks.test.cjs",
|
||||
"install-path-detection.test.cjs",
|
||||
"install-regressions.test.cjs",
|
||||
"install-runtime-artifacts.test.cjs",
|
||||
"install-update-marker.test.cjs",
|
||||
"install.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
*
|
||||
* Scans sdk/src/query/, sdk/src/, get-shit-done/bin/lib/, bin/ for production
|
||||
* modules, then counts matching test files in tests/ and sdk/src (recursive). Cap is 2
|
||||
* (primary + one integration). Over-limit clusters must be in the allowlist at
|
||||
* their frozen count (ratchet: may only decrease). --json emits structured output.
|
||||
* (primary + one integration). Over-limit clusters must be in the allowlist with the
|
||||
* EXACT set of test filenames grandfathered (identity ratchet via allowlist-ratchet.cjs).
|
||||
* Adding a new test file to a capped module is a novel offender; removing one requires
|
||||
* pruning the allowlist entry (stale). --json emits structured output.
|
||||
*
|
||||
* Verdicts: OK_UNDER_LIMIT | OK_IN_ALLOWLIST | FAIL_EXCEEDS_LIMIT |
|
||||
* FAIL_EXCEEDS_ALLOWLIST | HINT_CAN_REMOVE_FROM_ALLOWLIST
|
||||
* FAIL_NOVEL_FILES | FAIL_STALE_ALLOWLIST
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const PROD_DIRS = [
|
||||
@@ -30,11 +33,11 @@ const ALLOWLIST_PATH = path.join(__dirname, 'lint-test-file-count.allowlist.json
|
||||
const MAX_FILES = 2;
|
||||
|
||||
const Verdict = Object.freeze({
|
||||
OK_UNDER_LIMIT: 'OK_UNDER_LIMIT',
|
||||
OK_IN_ALLOWLIST: 'OK_IN_ALLOWLIST',
|
||||
FAIL_EXCEEDS_LIMIT: 'FAIL_EXCEEDS_LIMIT',
|
||||
FAIL_EXCEEDS_ALLOWLIST: 'FAIL_EXCEEDS_ALLOWLIST',
|
||||
HINT_CAN_REMOVE_FROM_ALLOWLIST: 'HINT_CAN_REMOVE_FROM_ALLOWLIST',
|
||||
OK_UNDER_LIMIT: 'OK_UNDER_LIMIT',
|
||||
OK_IN_ALLOWLIST: 'OK_IN_ALLOWLIST',
|
||||
FAIL_EXCEEDS_LIMIT: 'FAIL_EXCEEDS_LIMIT',
|
||||
FAIL_NOVEL_FILES: 'FAIL_NOVEL_FILES',
|
||||
FAIL_STALE_ALLOWLIST: 'FAIL_STALE_ALLOWLIST',
|
||||
});
|
||||
|
||||
function isTestFile(name) {
|
||||
@@ -120,17 +123,62 @@ function loadAllowlist() {
|
||||
catch (_) { return {}; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate one module's test files against the allowlist.
|
||||
*
|
||||
* For modules under the default cap (≤ MAX_FILES): simple count check.
|
||||
* For modules with an allowlist entry: identity check via assertWithinAllowlist.
|
||||
* - count now ≤ MAX_FILES → FAIL_STALE_ALLOWLIST (all known files are stale; prune entry)
|
||||
* - novel files (in current but not in known) → FAIL_NOVEL_FILES
|
||||
* - stale files (in known but not in current) → FAIL_STALE_ALLOWLIST
|
||||
* - exact match → OK_IN_ALLOWLIST
|
||||
* For modules over the cap with no allowlist entry: FAIL_EXCEEDS_LIMIT.
|
||||
*
|
||||
* Returns { verdict, prefix, count, knownFiles, novel, stale, files }
|
||||
*/
|
||||
function evaluateLint({ prefix, testFiles, allowlist }) {
|
||||
const count = testFiles.length;
|
||||
const entry = allowlist[prefix];
|
||||
const ceiling = entry ? entry.current : null;
|
||||
const currentNames = testFiles.map(f => path.basename(f));
|
||||
|
||||
if (entry !== undefined) {
|
||||
if (count <= MAX_FILES) return { verdict: Verdict.HINT_CAN_REMOVE_FROM_ALLOWLIST, prefix, count, ceiling, files: testFiles };
|
||||
if (count <= ceiling) return { verdict: Verdict.OK_IN_ALLOWLIST, prefix, count, ceiling, files: testFiles };
|
||||
return { verdict: Verdict.FAIL_EXCEEDS_ALLOWLIST, prefix, count, ceiling, files: testFiles };
|
||||
const knownFiles = Array.isArray(entry.files) ? entry.files : [];
|
||||
// If the module is now at or under the default cap, the entire allowlist entry is
|
||||
// stale and must be removed — this is a ratchet-DOWN failure, not a hint.
|
||||
// All known files are stale (the whole entry can go).
|
||||
if (count <= MAX_FILES) {
|
||||
return {
|
||||
verdict: Verdict.FAIL_STALE_ALLOWLIST,
|
||||
prefix, count,
|
||||
knownFiles,
|
||||
novel: [],
|
||||
stale: knownFiles.slice().sort(),
|
||||
files: testFiles,
|
||||
};
|
||||
}
|
||||
// Identity check via assertWithinAllowlist
|
||||
const messages = [];
|
||||
const { novel, stale } = assertWithinAllowlist({
|
||||
label: prefix,
|
||||
current: currentNames,
|
||||
known: knownFiles,
|
||||
fail: (msg) => messages.push(msg),
|
||||
pruneHint: 'scripts/lint-test-file-count.allowlist.json',
|
||||
});
|
||||
|
||||
if (novel.length > 0) {
|
||||
return { verdict: Verdict.FAIL_NOVEL_FILES, prefix, count, knownFiles, novel, stale, files: testFiles };
|
||||
}
|
||||
if (stale.length > 0) {
|
||||
return { verdict: Verdict.FAIL_STALE_ALLOWLIST, prefix, count, knownFiles, novel, stale, files: testFiles };
|
||||
}
|
||||
return { verdict: Verdict.OK_IN_ALLOWLIST, prefix, count, knownFiles, novel: [], stale: [], files: testFiles };
|
||||
}
|
||||
if (count <= MAX_FILES) return { verdict: Verdict.OK_UNDER_LIMIT, prefix, count, ceiling: null, files: testFiles };
|
||||
return { verdict: Verdict.FAIL_EXCEEDS_LIMIT, prefix, count, ceiling: null, files: testFiles };
|
||||
|
||||
if (count <= MAX_FILES) {
|
||||
return { verdict: Verdict.OK_UNDER_LIMIT, prefix, count, knownFiles: null, novel: [], stale: [], files: testFiles };
|
||||
}
|
||||
return { verdict: Verdict.FAIL_EXCEEDS_LIMIT, prefix, count, knownFiles: null, novel: [], stale: [], files: testFiles };
|
||||
}
|
||||
|
||||
function run() {
|
||||
@@ -147,35 +195,42 @@ function run() {
|
||||
}
|
||||
|
||||
const failures = results.filter(r =>
|
||||
r.verdict === Verdict.FAIL_EXCEEDS_LIMIT || r.verdict === Verdict.FAIL_EXCEEDS_ALLOWLIST);
|
||||
const hints = results.filter(r => r.verdict === Verdict.HINT_CAN_REMOVE_FROM_ALLOWLIST);
|
||||
r.verdict === Verdict.FAIL_EXCEEDS_LIMIT ||
|
||||
r.verdict === Verdict.FAIL_NOVEL_FILES ||
|
||||
r.verdict === Verdict.FAIL_STALE_ALLOWLIST);
|
||||
|
||||
if (jsonMode) {
|
||||
console.log(JSON.stringify({ ok: failures.length === 0, results, failures, hints }, null, 2));
|
||||
console.log(JSON.stringify({ ok: failures.length === 0, results, failures, hints: [] }, null, 2));
|
||||
process.exit(failures.length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
if (failures.length === 0) {
|
||||
const inAllowlist = results.filter(r => r.verdict === Verdict.OK_IN_ALLOWLIST).length;
|
||||
console.log(`ok lint-test-file-count: ${results.length} module(s) checked, 0 failures` +
|
||||
(inAllowlist > 0 ? `, ${inAllowlist} allowlisted` : '') +
|
||||
(hints.length > 0 ? `, ${hints.length} hint(s)` : ''));
|
||||
for (const h of hints) {
|
||||
console.log(` hint: "${h.prefix}" is allowlisted at ${h.ceiling} but now has ${h.count} — remove from allowlist`);
|
||||
}
|
||||
(inAllowlist > 0 ? `, ${inAllowlist} allowlisted` : ''));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stderr.write(`\nERROR lint-test-file-count: ${failures.length} module(s) exceed the test-file limit\n\n`);
|
||||
for (const f of failures) {
|
||||
const tag = f.verdict === Verdict.FAIL_EXCEEDS_LIMIT
|
||||
? `${f.count} files (limit ${MAX_FILES})`
|
||||
: `${f.count} files (allowlist ceiling ${f.ceiling})`;
|
||||
process.stderr.write(` ${f.prefix}: ${tag}\n`);
|
||||
for (const tf of f.files) process.stderr.write(` ${path.relative(ROOT, tf)}\n`);
|
||||
if (f.verdict === Verdict.FAIL_EXCEEDS_LIMIT) {
|
||||
process.stderr.write(` ${f.prefix}: ${f.count} files (limit ${MAX_FILES}) — not in allowlist\n`);
|
||||
for (const tf of f.files) process.stderr.write(` ${path.relative(ROOT, tf)}\n`);
|
||||
} else if (f.verdict === Verdict.FAIL_NOVEL_FILES) {
|
||||
process.stderr.write(` ${f.prefix}: ${f.novel.length} NEW test file(s) not in allowlist\n`);
|
||||
for (const n of f.novel) process.stderr.write(` + ${n}\n`);
|
||||
} else if (f.verdict === Verdict.FAIL_STALE_ALLOWLIST) {
|
||||
if (f.count <= MAX_FILES) {
|
||||
const staleList = f.stale.join(', ');
|
||||
process.stderr.write(` "${f.prefix}": now at ${f.count} file(s) (≤ ${MAX_FILES}) — remove its entry from the allowlist (stale: ${staleList})\n`);
|
||||
} else {
|
||||
process.stderr.write(` ${f.prefix}: ${f.stale.length} allowlisted file(s) no longer present — prune allowlist\n`);
|
||||
for (const s of f.stale) process.stderr.write(` - ${s}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
process.stderr.write('\nFix: consolidate test files per module (one primary + one integration).\n');
|
||||
process.stderr.write('Or add the module to scripts/lint-test-file-count.allowlist.json with PR justification.\n\n');
|
||||
process.stderr.write('Or update scripts/lint-test-file-count.allowlist.json with PR justification.\n\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
const { runGsdTools, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function mkplanning(base) {
|
||||
const planningDir = path.join(base, '.planning');
|
||||
@@ -84,7 +84,7 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W
|
||||
fs.mkdirSync(path.join(phasesDir, '999.1-foo'), { recursive: true });
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('no W005 for 999.1-foo (multi-digit sub-phase prefix)', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
@@ -159,7 +159,7 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
|
||||
);
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('no W006 for Phase 64 archived under milestones/v1.0-phases/', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
@@ -227,7 +227,7 @@ describe('Drift item I001 — canonicalPlanStem: long PLAN stem matches short SU
|
||||
fs.writeFileSync(path.join(phaseDir, '68-01-SUMMARY.md'), '# Summary\n');
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('no I001 when 68-01-scaffolding-PLAN.md matches 68-01-SUMMARY.md via canonicalPlanStem', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
|
||||
@@ -30,6 +30,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ── Load cmdPhaseComplete directly from phase.cjs (bypass the SDK router) ────
|
||||
// phase-command-router.cjs delegates to SDK when available; we must test the
|
||||
// CJS implementation directly since that is where the bug lives.
|
||||
@@ -216,7 +218,7 @@ describe('issue #4 (CJS): cmdPhaseComplete — idempotency (blind-increment bug)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('T1: double invocation does NOT double-increment Completed Phases in STATE.md body', () => {
|
||||
@@ -361,7 +363,7 @@ describe('issue #4 (CJS): cmdPhaseComplete — progress percent clamp', () => {
|
||||
let tmpDir;
|
||||
|
||||
afterEach(() => {
|
||||
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('T2: Progress percent never exceeds 100 after double invocation', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
const { runGsdTools, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ── Fixture helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('Drift item 1 — W007 activeDiskPhases: no false W007 for archived pha
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('no W007 for archived phase "1" absent from current ROADMAP', () => {
|
||||
@@ -198,7 +198,7 @@ describe('Drift item 2 — phaseVariants() normalization: letter-suffix zero-pad
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('no false W006 when ROADMAP says 01A and disk has 1A-... (phaseVariants normalizes)', () => {
|
||||
@@ -275,7 +275,7 @@ describe('Drift item 3 — W006 false positive when disk has zero-padded letter
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('no false W006 when ROADMAP says 3B and disk has 03B-... (phaseVariants covers zero-padded)', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
validateWorkstreamName,
|
||||
@@ -119,7 +120,7 @@ describe('active-workstream-store', () => {
|
||||
} finally {
|
||||
if (savedSession !== undefined) process.env.GSD_SESSION_KEY = savedSession;
|
||||
else delete process.env.GSD_SESSION_KEY;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,7 +140,7 @@ describe('active-workstream-store', () => {
|
||||
assert.equal(active, null);
|
||||
assert.equal(adapter.read(), null);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ const {
|
||||
resolveRelativeDependency,
|
||||
} = require('../scripts/affected-tests-lib.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -188,7 +190,7 @@ test('transitive: changing a deep dependency selects the test that depends on it
|
||||
'get-shit-done/bin/lib/depA.cjs': `'use strict';\nconst depB = require('./depB.cjs');\nmodule.exports = { a: depB };\n`,
|
||||
'tests/t.test.cjs': `'use strict';\nconst depA = require('../get-shit-done/bin/lib/depA.cjs');\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/t.test.cjs']);
|
||||
const selected = pickAffectedTests(
|
||||
@@ -213,7 +215,7 @@ test('adversarial(a): cycle depA<->depB — changing depA selects test, no hang'
|
||||
'get-shit-done/bin/lib/depB.cjs': `'use strict';\nconst depA = require('./depA.cjs');\nmodule.exports = {};\n`,
|
||||
'tests/cycle.test.cjs': `'use strict';\nconst depA = require('../get-shit-done/bin/lib/depA.cjs');\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Must complete without hanging
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/cycle.test.cjs']);
|
||||
@@ -234,7 +236,7 @@ test('adversarial(b): missing require (gone file) — null resolve, no crash', (
|
||||
// Requires a file that does not exist
|
||||
'tests/missing.test.cjs': `'use strict';\nconst x = require('../get-shit-done/bin/lib/gone.cjs');\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Should not throw
|
||||
let reverseIndex;
|
||||
@@ -258,7 +260,7 @@ test('adversarial(c): .json dependency — changing data.json selects the test',
|
||||
'get-shit-done/bin/lib/data.json': `{"key":"value"}`,
|
||||
'tests/json.test.cjs': `'use strict';\nconst data = require('../get-shit-done/bin/lib/data.json');\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/json.test.cjs']);
|
||||
const selected = pickAffectedTests(
|
||||
@@ -279,7 +281,7 @@ test('adversarial(d): re-export chain — changing depB selects the test that re
|
||||
'get-shit-done/bin/lib/reexporter.cjs': `'use strict';\nmodule.exports = require('./depB.cjs');\n`,
|
||||
'tests/reexport.test.cjs': `'use strict';\nconst x = require('../get-shit-done/bin/lib/reexporter.cjs');\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/reexport.test.cjs']);
|
||||
const selected = pickAffectedTests(
|
||||
@@ -313,7 +315,7 @@ test('adversarial(f): WIDEN — changing a src file with no test dependents wide
|
||||
'get-shit-done/bin/lib/orphan.cjs': `'use strict';\nmodule.exports = {};\n`,
|
||||
'tests/unrelated.test.cjs': `'use strict';\n// requires nothing\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/unrelated.test.cjs']);
|
||||
|
||||
@@ -345,7 +347,7 @@ test('adversarial(g): dynamic require in changed file with no static dependents
|
||||
'get-shit-done/bin/lib/dynamic.cjs': `'use strict';\nconst x = 'foo';\nconst m = require(\`./\${x}\`);\nmodule.exports = {};\n`,
|
||||
'tests/unrelated.test.cjs': `'use strict';\n// does not require dynamic.cjs\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const reverseIndex = buildTransitiveReverseIndex(dir, ['tests/unrelated.test.cjs']);
|
||||
|
||||
@@ -367,7 +369,7 @@ test('resolveRelativeDependency resolves .ts, .json extensions', (t) => {
|
||||
'src/helper.ts': `export const x = 1;\n`,
|
||||
'src/data.json': `{"k":1}`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const fromAbs = path.join(dir, 'tests/consumer.cjs');
|
||||
|
||||
@@ -444,7 +446,7 @@ test('regression(mixed-diff): widen plan covers integration suite; concrete matc
|
||||
'tests/server.integration.test.cjs': `'use strict';\nconst s = require('../bin/lib/server.cjs');\n`,
|
||||
'tests/unrelated.test.cjs': `'use strict';\n// requires nothing\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Act: build graph over both test files
|
||||
const allTests = [
|
||||
@@ -532,7 +534,7 @@ test('regression(delete-only-source): deleting a source file triggers widen, nev
|
||||
'get-shit-done/bin/lib/other.cjs': `'use strict';\nmodule.exports = {};\n`,
|
||||
'tests/unrelated.test.cjs': `'use strict';\n// requires nothing from gone.cjs\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Act
|
||||
const allTests = ['tests/unrelated.test.cjs'];
|
||||
@@ -592,7 +594,7 @@ test('regression(rename-stale-old-path): deleted old path triggers widen, protec
|
||||
'tests/newname.test.cjs': `'use strict';\nconst x = require('../get-shit-done/bin/lib/newname.cjs');\n`,
|
||||
'tests/unrelated.test.cjs': `'use strict';\n// no dependency on oldname or newname\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Act
|
||||
const allTests = ['tests/newname.test.cjs', 'tests/unrelated.test.cjs'];
|
||||
@@ -646,7 +648,7 @@ test('regression(delete-only-test): deleting a test file does not trigger widen
|
||||
const dir = makeFixture({
|
||||
'tests/surviving.test.cjs': `'use strict';\n// a plain surviving unit test\n`,
|
||||
});
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
// Act
|
||||
// allTests comes from the fixture's tests/ directory — gone.test.cjs is absent.
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
* rationale in the PR, and make sure the bloat is not duplicated content
|
||||
* that belongs in `get-shit-done/references/`.
|
||||
*
|
||||
* Tighten-only invariant (issue #597): ceilings track the tier high-water mark
|
||||
* within GRACE lines. Budgets may only decrease, never silently creep upward.
|
||||
* The assertTightCeiling() call below enforces this automatically.
|
||||
*
|
||||
* See: https://github.com/open-gsd/gsd-core/issues/2361
|
||||
*/
|
||||
|
||||
@@ -26,13 +30,23 @@ const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { assertTightCeiling } = require('../scripts/lib/allowlist-ratchet.cjs');
|
||||
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
const XL_BUDGET = 1600;
|
||||
// Ceilings tightened to actualMax + GRACE per the ratchet-down rule (#597).
|
||||
// XL ceiling lowered from 1600 → 1512 (actualMax=1452, gsd-debugger).
|
||||
const XL_BUDGET = 1512;
|
||||
// LARGE ceiling kept at 1000 (actualMax=978, slack=22 ≤ GRACE=60).
|
||||
const LARGE_BUDGET = 1000;
|
||||
// DEFAULT ceiling kept at 500 (actualMax=495, slack=5 ≤ GRACE=60).
|
||||
const DEFAULT_BUDGET = 500;
|
||||
|
||||
// Grace band: maximum allowed slack (ceiling − actualMax) before a ceiling is
|
||||
// considered too loose. 60 lines gives one reasonable screen of breathing room
|
||||
// without permitting gross inflation.
|
||||
const GRACE = 60;
|
||||
|
||||
const XL_AGENTS = new Set([
|
||||
'gsd-debugger',
|
||||
'gsd-planner',
|
||||
@@ -83,6 +97,35 @@ describe('SIZE: agent line-count budget', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('SIZE: tier anti-creep (tighten-only ceilings, issue #597)', () => {
|
||||
// For each tier, compute the high-water mark across all files in that tier
|
||||
// and assert the ceiling stays tight. Prevents budgets from silently drifting
|
||||
// upward: ceiling − actualMax must not exceed GRACE.
|
||||
test('XL tier: ceiling tracks high-water mark within GRACE', () => {
|
||||
const values = ALL_AGENTS
|
||||
.filter(a => XL_AGENTS.has(a))
|
||||
.map(a => lineCount(path.join(AGENTS_DIR, a + '.md')));
|
||||
const actualMax = Math.max(...values);
|
||||
assertTightCeiling({ label: 'XL', actualMax, ceiling: XL_BUDGET, grace: GRACE, fail: assert.fail });
|
||||
});
|
||||
|
||||
test('LARGE tier: ceiling tracks high-water mark within GRACE', () => {
|
||||
const values = ALL_AGENTS
|
||||
.filter(a => LARGE_AGENTS.has(a))
|
||||
.map(a => lineCount(path.join(AGENTS_DIR, a + '.md')));
|
||||
const actualMax = Math.max(...values);
|
||||
assertTightCeiling({ label: 'LARGE', actualMax, ceiling: LARGE_BUDGET, grace: GRACE, fail: assert.fail });
|
||||
});
|
||||
|
||||
test('DEFAULT tier: ceiling tracks high-water mark within GRACE', () => {
|
||||
const values = ALL_AGENTS
|
||||
.filter(a => !XL_AGENTS.has(a) && !LARGE_AGENTS.has(a))
|
||||
.map(a => lineCount(path.join(AGENTS_DIR, a + '.md')));
|
||||
const actualMax = Math.max(...values);
|
||||
assertTightCeiling({ label: 'DEFAULT', actualMax, ceiling: DEFAULT_BUDGET, grace: GRACE, fail: assert.fail });
|
||||
});
|
||||
});
|
||||
|
||||
describe('SIZE: every agent is classified', () => {
|
||||
test('every agent falls in exactly one tier', () => {
|
||||
for (const agent of ALL_AGENTS) {
|
||||
|
||||
@@ -263,7 +263,7 @@ describe('agent-skills global: prefix', () => {
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
cleanup(fakeHome);
|
||||
});
|
||||
|
||||
function createGlobalSkill(name) {
|
||||
|
||||
297
tests/allowlist-ratchet.test.cjs
Normal file
297
tests/allowlist-ratchet.test.cjs
Normal file
@@ -0,0 +1,297 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tests for scripts/lib/allowlist-ratchet.cjs
|
||||
*
|
||||
* Covers assertWithinAllowlist and assertTightCeiling.
|
||||
* Uses a non-throwing fake `fail` that records messages into an array so we can
|
||||
* assert on call count and message content without early-exit on first failure.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
assertWithinAllowlist,
|
||||
assertTightCeiling,
|
||||
} = require('../scripts/lib/allowlist-ratchet.cjs');
|
||||
|
||||
// ─── Fake fail helper ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns a { fail, calls } pair. `fail` records its message without throwing,
|
||||
* so tests can observe every violation rather than stopping at the first.
|
||||
*/
|
||||
function makeFail() {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
fail(msg) {
|
||||
calls.push(msg);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── assertWithinAllowlist ───────────────────────────────────────────────────
|
||||
|
||||
describe('assertWithinAllowlist', () => {
|
||||
test('clean case: current subset of known, no stale entries — fail never called', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'test-guard',
|
||||
current: ['a.ts', 'b.ts'],
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 0, 'fail should not be called');
|
||||
assert.deepStrictEqual(result.novel, []);
|
||||
assert.deepStrictEqual(result.stale, []);
|
||||
});
|
||||
|
||||
test('novel detected: id in current but not in known — fail called with that id', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'novel-guard',
|
||||
current: ['a.ts', 'b.ts', 'c.ts'],
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1, 'fail should be called once for novel');
|
||||
assert.ok(calls[0].includes('c.ts'), 'message should mention the novel id');
|
||||
assert.ok(
|
||||
calls[0].includes('fix at the source'),
|
||||
'message should include fix-at-source guidance'
|
||||
);
|
||||
assert.deepStrictEqual(result.novel, ['c.ts']);
|
||||
assert.deepStrictEqual(result.stale, []);
|
||||
});
|
||||
|
||||
test('stale detected: id in known but not in current — fail called with that id', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'stale-guard',
|
||||
current: ['a.ts'],
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1, 'fail should be called once for stale');
|
||||
assert.ok(calls[0].includes('b.ts'), 'message should mention the stale id');
|
||||
assert.ok(
|
||||
calls[0].includes('ratchets toward zero'),
|
||||
'message should include ratchet-toward-zero language'
|
||||
);
|
||||
assert.deepStrictEqual(result.novel, []);
|
||||
assert.deepStrictEqual(result.stale, ['b.ts']);
|
||||
});
|
||||
|
||||
test('stale message includes pruneHint when provided', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
assertWithinAllowlist({
|
||||
label: 'prune-guard',
|
||||
current: ['a.ts'],
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
pruneHint: 'edit scripts/my-allowlist.json',
|
||||
});
|
||||
assert.ok(
|
||||
calls[0].includes('edit scripts/my-allowlist.json'),
|
||||
'message should include the pruneHint'
|
||||
);
|
||||
});
|
||||
|
||||
test('both novel and stale at once — fail called twice', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'both-guard',
|
||||
current: ['a.ts', 'c.ts'], // c.ts is new, b.ts is fixed
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 2, 'fail should be called once for novel and once for stale');
|
||||
const allMessages = calls.join('\n');
|
||||
assert.ok(allMessages.includes('c.ts'), 'should mention novel id c.ts');
|
||||
assert.ok(allMessages.includes('b.ts'), 'should mention stale id b.ts');
|
||||
assert.deepStrictEqual(result.novel, ['c.ts']);
|
||||
assert.deepStrictEqual(result.stale, ['b.ts']);
|
||||
});
|
||||
|
||||
test('empty inputs — fail never called', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'empty-guard',
|
||||
current: [],
|
||||
known: [],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 0);
|
||||
assert.deepStrictEqual(result.novel, []);
|
||||
assert.deepStrictEqual(result.stale, []);
|
||||
});
|
||||
|
||||
test('order-independence: Sets and arrays produce the same result', () => {
|
||||
const callsArr = makeFail();
|
||||
const callsSet = makeFail();
|
||||
|
||||
const resultArr = assertWithinAllowlist({
|
||||
label: 'order-array',
|
||||
current: ['z.ts', 'a.ts', 'm.ts'],
|
||||
known: ['a.ts', 'm.ts'],
|
||||
fail: callsArr.fail,
|
||||
});
|
||||
|
||||
const resultSet = assertWithinAllowlist({
|
||||
label: 'order-set',
|
||||
current: new Set(['z.ts', 'a.ts', 'm.ts']),
|
||||
known: new Set(['a.ts', 'm.ts']),
|
||||
fail: callsSet.fail,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(resultArr.novel, resultSet.novel, 'novel should be identical regardless of input type');
|
||||
assert.deepStrictEqual(resultArr.stale, resultSet.stale, 'stale should be identical regardless of input type');
|
||||
assert.deepStrictEqual(resultArr.novel, ['z.ts'], 'novel should be sorted');
|
||||
});
|
||||
|
||||
test('returned novel and stale arrays are sorted', () => {
|
||||
const { fail } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'sort-guard',
|
||||
current: ['z.ts', 'a.ts', 'm.ts', 'new.ts'],
|
||||
known: ['z.ts', 'a.ts', 'm.ts', 'old.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.deepStrictEqual(result.novel, ['new.ts']);
|
||||
assert.deepStrictEqual(result.stale, ['old.ts']);
|
||||
});
|
||||
|
||||
test('current empty, known non-empty — all known are stale', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'all-stale',
|
||||
current: [],
|
||||
known: ['a.ts', 'b.ts'],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1);
|
||||
assert.deepStrictEqual(result.stale, ['a.ts', 'b.ts']);
|
||||
assert.deepStrictEqual(result.novel, []);
|
||||
});
|
||||
|
||||
test('known empty, current non-empty — all current are novel', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertWithinAllowlist({
|
||||
label: 'all-novel',
|
||||
current: ['a.ts', 'b.ts'],
|
||||
known: [],
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1);
|
||||
assert.deepStrictEqual(result.novel, ['a.ts', 'b.ts']);
|
||||
assert.deepStrictEqual(result.stale, []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── assertTightCeiling ──────────────────────────────────────────────────────
|
||||
|
||||
describe('assertTightCeiling', () => {
|
||||
test('actualMax under ceiling within grace — ok, fail never called', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertTightCeiling({
|
||||
label: 'size-guard',
|
||||
actualMax: 90,
|
||||
ceiling: 100,
|
||||
grace: 15,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 0, 'fail should not be called');
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.slack, 10);
|
||||
});
|
||||
|
||||
test('actualMax over ceiling — fail called with regression message', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertTightCeiling({
|
||||
label: 'size-guard',
|
||||
actualMax: 110,
|
||||
ceiling: 100,
|
||||
grace: 5,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1, 'fail should be called once');
|
||||
assert.ok(calls[0].includes('Regression'), 'message should say Regression');
|
||||
assert.ok(calls[0].includes('110'), 'message should include actualMax');
|
||||
assert.ok(calls[0].includes('100'), 'message should include ceiling');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.slack, -10);
|
||||
});
|
||||
|
||||
test('ceiling too loose (slack > grace) — fail called with tighten message', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertTightCeiling({
|
||||
label: 'loose-guard',
|
||||
actualMax: 50,
|
||||
ceiling: 100,
|
||||
grace: 10,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1, 'fail should be called once');
|
||||
assert.ok(
|
||||
calls[0].toLowerCase().includes('tighten') || calls[0].includes('too far'),
|
||||
'message should mention tightening'
|
||||
);
|
||||
assert.ok(calls[0].includes('Budgets may only decrease'), 'message should include budget policy');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.slack, 50);
|
||||
});
|
||||
|
||||
test('boundary: slack === grace — ok (exactly at the grace limit)', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertTightCeiling({
|
||||
label: 'boundary-guard',
|
||||
actualMax: 90,
|
||||
ceiling: 100,
|
||||
grace: 10,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 0, 'fail should not be called at exact grace boundary');
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.slack, 10);
|
||||
});
|
||||
|
||||
test('actualMax equals ceiling — ok, slack is zero', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
const result = assertTightCeiling({
|
||||
label: 'exact-guard',
|
||||
actualMax: 100,
|
||||
ceiling: 100,
|
||||
grace: 0,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 0);
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.slack, 0);
|
||||
});
|
||||
|
||||
test('grace 0: any slack triggers fail', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
assertTightCeiling({
|
||||
label: 'tight-guard',
|
||||
actualMax: 99,
|
||||
ceiling: 100,
|
||||
grace: 0,
|
||||
fail,
|
||||
});
|
||||
assert.strictEqual(calls.length, 1, 'any slack above 0 should fail when grace is 0');
|
||||
});
|
||||
|
||||
test('label appears in failure messages', () => {
|
||||
const { fail, calls } = makeFail();
|
||||
assertTightCeiling({
|
||||
label: 'my-special-guard',
|
||||
actualMax: 200,
|
||||
ceiling: 100,
|
||||
grace: 5,
|
||||
fail,
|
||||
});
|
||||
assert.ok(calls[0].includes('my-special-guard'), 'label should appear in message');
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
// The helpers under test.
|
||||
const { runNpm, isolatedNpmEnv } = require('./helpers.cjs');
|
||||
const { runNpm, isolatedNpmEnv, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// Resolve a filesystem path to its canonical (symlink-free) form even if the
|
||||
// leaf does not exist yet (e.g. ~/.npm before npm has written its cache).
|
||||
@@ -113,9 +113,9 @@ describe('bug-131: runNpm isolates HOME from the caller environment', () => {
|
||||
`expected semver output from npm --version, got: ${stdout}`,
|
||||
);
|
||||
} finally {
|
||||
// Restore write permission before cleanup so rmSync can delete it.
|
||||
// Restore write permission before cleanup so the directory can be deleted.
|
||||
try { fs.chmodSync(poisonedHome, 0o700); } catch (_) { /* best-effort */ }
|
||||
fs.rmSync(poisonedHome, { recursive: true, force: true });
|
||||
cleanup(poisonedHome);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ function createTempDir(prefix) {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup wrapper; try/catch swallows ENOENT so runInstaller teardown never fails the test
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ function createFakeInstall(prefix = 'gsd-uninstall-test-') {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local teardown helper predates helpers.cjs; renaming would collide with the imported cleanup
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ function createTempDir(prefix) {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup() helper wrapping rmSync; cannot use imported cleanup() without naming collision
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const state = require('../get-shit-done/bin/lib/state.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
describe('buildStateFrontmatter cache invalidation (#1967)', () => {
|
||||
let tmpDir;
|
||||
@@ -59,7 +60,7 @@ describe('buildStateFrontmatter cache invalidation (#1967)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('writeStateMd invalidates cache so subsequent reads see new disk state', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const WORKFLOWS_DIR = path.join(__dirname, '..', 'get-shit-done', 'workflows');
|
||||
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
|
||||
@@ -126,8 +127,8 @@ describe('bug-211: launcher ~/.claude home fallback', () => {
|
||||
`Expected stub output "CLAUDE_HOME_STUB:ping,test", got:\n${stdout.trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
fs.rmSync(fakeRuntime, { recursive: true, force: true });
|
||||
cleanup(fakeHome);
|
||||
cleanup(fakeRuntime);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,8 +183,8 @@ describe('bug-211: launcher ~/.claude home fallback', () => {
|
||||
`Expected stderr to contain "not found" or "ERROR", got: ${stderrOutput.trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
fs.rmSync(fakeRuntime, { recursive: true, force: true });
|
||||
cleanup(fakeHome);
|
||||
cleanup(fakeRuntime);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ function createTempDir(prefix) {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup() helper wrapping rmSync; cannot use imported cleanup() without naming collision
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const {
|
||||
getCodexSkillAdapterHeader,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const makeTmp = (prefix) => createTempDir(`gsd-2256-${prefix}-`);
|
||||
|
||||
function writeJson(p, obj) {
|
||||
@@ -36,10 +36,6 @@ function writeJson(p, obj) {
|
||||
fs.writeFileSync(p, JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
function rmr(p) {
|
||||
try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
describe('bug #2256 — readGsdEffectiveModelOverrides', () => {
|
||||
let projectDir;
|
||||
let homeDir;
|
||||
@@ -65,8 +61,8 @@ describe('bug #2256 — readGsdEffectiveModelOverrides', () => {
|
||||
if (origUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = origUserProfile;
|
||||
}
|
||||
rmr(projectDir);
|
||||
rmr(homeDir);
|
||||
cleanup(projectDir);
|
||||
cleanup(homeDir);
|
||||
});
|
||||
|
||||
test('returns null when neither source defines model_overrides', () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync, execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-worktree-path-guard.js');
|
||||
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
@@ -97,8 +98,8 @@ before(() => {
|
||||
after(() => {
|
||||
// Remove worktree registration before deleting the directory
|
||||
try { git(mainRepo, ['worktree', 'remove', '--force', worktreeDir]); } catch { /* ignore */ }
|
||||
try { fs.rmSync(mainRepo, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
cleanup(mainRepo);
|
||||
cleanup(worktreeDir);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -7,6 +7,8 @@ const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-workflow-guard.js');
|
||||
|
||||
function git(cwd, args) {
|
||||
@@ -61,7 +63,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(envelope.decision, 'block');
|
||||
assert.strictEqual(envelope.code, 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,7 +75,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 2);
|
||||
assert.strictEqual(JSON.parse(result.stdout).code, 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,7 +87,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,7 +99,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,7 +111,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +123,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,7 +134,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -154,7 +156,7 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
/WORKFLOW ADVISORY/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,8 @@ const {
|
||||
parseTomlToObject,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
if (previousGsdTestMode === undefined) {
|
||||
delete process.env.GSD_TEST_MODE;
|
||||
} else {
|
||||
@@ -108,7 +110,7 @@ describe('#2760 defect 3 — Hooks AoT preservation across install/uninstall/rei
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('fresh install emits the two-level nested AoT schema (#2773)', () => {
|
||||
@@ -249,7 +251,7 @@ describe('#2760 fix 2 — Strip purges invalid legacy [agents] / [[agents]] rega
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('strips bare [agents] single-bracket block (no GSD marker, arbitrary user keys)', () => {
|
||||
@@ -345,7 +347,7 @@ describe('#2760 fix 3 — Post-write Codex schema validation', { concurrency: fa
|
||||
const result = validateCodexConfigSchema(content);
|
||||
assert.equal(result.ok, true, 'GSD-emitted config passes schema validation');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -416,7 +418,7 @@ describe('#2760 fix 3 — Post-write Codex schema validation', { concurrency: fa
|
||||
);
|
||||
} finally {
|
||||
delete installModule.__codexSchemaValidator;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -474,7 +476,7 @@ describe('#2760 fix 4 — Write-failure rollback (atomic write + snapshot restor
|
||||
afterEach(() => {
|
||||
fs.renameSync = originalRenameSync;
|
||||
fs.writeFileSync = originalWriteFileSync;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('pre-install config bytes survive when fs.renameSync throws over configPath', () => {
|
||||
@@ -616,7 +618,7 @@ describe('#2760 CR4 finding 2 — Legacy flat [[hooks]] block migrates to namesp
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('pre-install legacy flat [[hooks]] gsd-check-update + user namespaced [[hooks.SessionStart]] → post-install converges on namespaced AoT', () => {
|
||||
@@ -766,7 +768,7 @@ describe('#2760 CR4 finding 1 — atomicWriteFileSync failure aborts install (po
|
||||
afterEach(() => {
|
||||
fs.renameSync = originalRenameSync;
|
||||
console.log = originalConsoleLog;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install throws and never prints "Done!" when atomicWriteFileSync fails on configPath', () => {
|
||||
@@ -846,7 +848,7 @@ describe('#2760 CR5 finding 1 — pre-write failures abort install (outer catch
|
||||
afterEach(() => {
|
||||
console.log = originalConsoleLog;
|
||||
delete installModule.__codexSchemaValidator;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('pre-write throw (validator throws, not returns {ok:false}) is fatal and restores snapshot', () => {
|
||||
@@ -996,7 +998,7 @@ describe('#2760 CR5 finding 3 — migration emits namespaced AoT (no flat/namesp
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('user has [[hooks.AfterTool]] AND legacy [hooks.SessionStart] → post-install both namespaced, no flat AoT', () => {
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('manifest path safety', () => {
|
||||
outside = path.join(tmpDir, '..', `outside-managed-file-${path.basename(tmpDir)}.txt`);
|
||||
});
|
||||
afterEach(() => {
|
||||
if (outside) fs.rmSync(outside, { recursive: true, force: true });
|
||||
cleanup(outside);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const {
|
||||
install,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const makeTmp = (prefix) => createTempDir(`gsd-2794-${prefix}-`);
|
||||
|
||||
function writeJson(p, obj) {
|
||||
@@ -43,9 +43,6 @@ function writeJson(p, obj) {
|
||||
fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function rmr(p) {
|
||||
try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
describe('bug-2794: readGsdRuntimeProfileResolver resolves opencode tier overrides', () => {
|
||||
let projectDir;
|
||||
@@ -62,8 +59,8 @@ describe('bug-2794: readGsdRuntimeProfileResolver resolves opencode tier overrid
|
||||
afterEach(() => {
|
||||
if (origHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = origHome;
|
||||
rmr(projectDir);
|
||||
rmr(homeDir);
|
||||
cleanup(projectDir);
|
||||
cleanup(homeDir);
|
||||
});
|
||||
|
||||
test('resolves opencode sonnet tier to user-supplied model ID', () => {
|
||||
@@ -127,8 +124,8 @@ describe('bug-2794: OpenCode agent install embeds model_profile_overrides model'
|
||||
if (origHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = origHome;
|
||||
process.chdir(origCwd);
|
||||
rmr(projectDir);
|
||||
rmr(homeDir);
|
||||
cleanup(projectDir);
|
||||
cleanup(homeDir);
|
||||
});
|
||||
|
||||
test('generated OpenCode agent frontmatter includes model from model_profile_overrides', () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
let computePathPrefix;
|
||||
|
||||
@@ -148,7 +149,7 @@ describe('bug-2831: OpenCode pathPrefix uses absolute path on all platforms', ()
|
||||
`output should include absolute path with @ prefix; got:\n${content}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,15 +23,12 @@ const os = require('node:os');
|
||||
|
||||
const auditModule = require('../get-shit-done/bin/lib/audit.cjs');
|
||||
const { auditOpenArtifacts } = auditModule;
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
function mkTmp() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'bug-2836-'));
|
||||
}
|
||||
|
||||
function rmTmp(dir) {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
|
||||
describe('bug #2836: audit-open quick-task summary filename + UAT terminal status', () => {
|
||||
// Ensure GSD env vars do not redirect planningDir() away from our fixture.
|
||||
let prevProject, prevWorkstream;
|
||||
@@ -70,7 +67,7 @@ describe('bug #2836: audit-open quick-task summary filename + UAT terminal statu
|
||||
);
|
||||
assert.equal(result.counts.quick_tasks, 0);
|
||||
} finally {
|
||||
rmTmp(cwd);
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,7 +92,7 @@ describe('bug #2836: audit-open quick-task summary filename + UAT terminal statu
|
||||
);
|
||||
assert.equal(result.counts.uat_gaps, 0);
|
||||
} finally {
|
||||
rmTmp(cwd);
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,7 +111,7 @@ describe('bug #2836: audit-open quick-task summary filename + UAT terminal statu
|
||||
const realUatGaps = result.items.uat_gaps.filter(i => !i.scan_error);
|
||||
assert.equal(realUatGaps.length, 0);
|
||||
} finally {
|
||||
rmTmp(cwd);
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -134,7 +131,7 @@ describe('bug #2836: audit-open quick-task summary filename + UAT terminal statu
|
||||
assert.equal(realUatGaps.length, 1, 'pending UAT must still be flagged');
|
||||
assert.equal(realUatGaps[0].status, 'pending');
|
||||
} finally {
|
||||
rmTmp(cwd);
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,7 +150,7 @@ describe('bug #2836: audit-open quick-task summary filename + UAT terminal statu
|
||||
assert.equal(realQuickTasks.length, 1);
|
||||
assert.equal(realQuickTasks[0].status, 'missing');
|
||||
} finally {
|
||||
rmTmp(cwd);
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,8 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const EXECUTE_PHASE_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
@@ -163,7 +165,7 @@ function runHandleBranchingStep(bash, cwd, branchName) {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).toString();
|
||||
} finally {
|
||||
fs.rmSync(scriptDir, { recursive: true, force: true });
|
||||
cleanup(scriptDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ describe('handle_branching branches off origin/HEAD, not current HEAD (#2916)',
|
||||
`new phase branch tip must equal ${upstream} tip`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -240,7 +242,7 @@ describe('handle_branching branches off origin/HEAD, not current HEAD (#2916)',
|
||||
'existing-branch tip must be preserved (no rebase/reset)'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ const { execFileSync } = require('node:child_process');
|
||||
|
||||
const GSD_TOOLS = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const { ERROR_REASON } = require(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs'));
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
describe('bug-2943: config-get returns schema default for context_window', () => {
|
||||
let tmpDir;
|
||||
@@ -39,7 +40,7 @@ describe('bug-2943: config-get returns schema default for context_window', () =>
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const cp = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
// Script lives at get-shit-done/bin/ so the installer ships it under
|
||||
@@ -47,7 +48,7 @@ function writeFile(absPath, content) {
|
||||
|
||||
function resetFixture({ withPristine = true } = {}) {
|
||||
for (const dir of [patchesDir, configDir, pristineDir]) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
fs.mkdirSync(patchesDir);
|
||||
fs.mkdirSync(configDir);
|
||||
@@ -79,7 +80,7 @@ before(() => {
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
cleanup(tmpRoot);
|
||||
});
|
||||
|
||||
describe('Bug #2969: deterministic Step 5 verification gate', () => {
|
||||
|
||||
@@ -40,6 +40,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const PROFILE_OUTPUT = path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'profile-output.cjs');
|
||||
const WORKFLOW = path.join(ROOT, 'get-shit-done', 'workflows', 'profile-user.md');
|
||||
@@ -85,7 +87,7 @@ describe('Bug #2973: dev-preferences default writer path is skills/gsd-dev-prefe
|
||||
assert.equal(fs.existsSync(legacyPath), false,
|
||||
`writer must not create ${legacyPath} (#2973)`);
|
||||
} finally {
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -130,7 +132,7 @@ describe('Bug #2973: installer migrates existing legacy dev-preferences.md to sk
|
||||
assert.equal(fs.existsSync(skillFile), true, `expected SKILL.md at ${skillFile}`);
|
||||
assert.equal(fs.readFileSync(skillFile, 'utf-8'), '# my legacy preferences\n');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -148,7 +150,7 @@ describe('Bug #2973: installer migrates existing legacy dev-preferences.md to sk
|
||||
// Existing content untouched.
|
||||
assert.equal(fs.readFileSync(skillFile, 'utf-8'), '# user-customized skill\n');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -200,7 +202,7 @@ describe('Bug #2973 (#3003 CR): installRuntimeArtifacts preserves user-owned gsd
|
||||
assert.equal(fs.readFileSync(skillFile, 'utf-8'), userContent,
|
||||
'user content must be byte-identical after the install');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -239,7 +241,7 @@ describe('Bug #2973 (#3003 CR): installRuntimeArtifacts preserves user-owned gsd
|
||||
assert.equal(fs.existsSync(path.join(staleSkillDir, 'SKILL.md')), true,
|
||||
'fresh SKILL.md from source must be installed after wipe');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const ROOT = path.join(__dirname, '..');
|
||||
const { auditWorkflowScriptPaths, AUDIT_FINDING } = require(
|
||||
path.join(ROOT, 'scripts', 'audit-workflow-script-paths.cjs'),
|
||||
);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// auditWorkflowScriptPaths is a pure function: it walks workflowsDir,
|
||||
// extracts every ${GSD_HOME}/<path> script reference, and returns a
|
||||
@@ -39,7 +40,7 @@ function fixtureRepo({ workflows, files }) {
|
||||
}
|
||||
|
||||
before(() => { tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2995-')); });
|
||||
after(() => { fs.rmSync(tmpRoot, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpRoot); });
|
||||
|
||||
describe('Bug #2995: post-install script-paths audit (#2995)', () => {
|
||||
test('AUDIT_FINDING enum exposes the documented codes', () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ const crypto = require('node:crypto');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
function sha256(content) {
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
@@ -52,7 +53,7 @@ describe('Bug #2998: populatePristineDir is exported and writes pristine for mod
|
||||
});
|
||||
assert.equal(written, 0);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,7 +88,7 @@ describe('Bug #2998: populatePristineDir is exported and writes pristine for mod
|
||||
const content = fs.readFileSync(out, 'utf-8');
|
||||
assert.ok(content.length > 0, 'pristine file should be non-empty');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,7 +108,7 @@ describe('Bug #2998: populatePristineDir is exported and writes pristine for mod
|
||||
const out = path.join(pristineDir, 'get-shit-done/this-path-does-not-exist.md');
|
||||
assert.equal(fs.existsSync(out), false, 'pristine should not contain ghost paths');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,8 +133,8 @@ describe('Bug #2998: populatePristineDir is exported and writes pristine for mod
|
||||
const b = fs.readFileSync(path.join(tmp2, 'gsd-pristine', candidate));
|
||||
assert.equal(sha256(a), sha256(b), 'two runs of the same inputs must yield identical pristine content');
|
||||
} finally {
|
||||
fs.rmSync(tmp1, { recursive: true, force: true });
|
||||
fs.rmSync(tmp2, { recursive: true, force: true });
|
||||
cleanup(tmp1);
|
||||
cleanup(tmp2);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -160,7 +161,7 @@ describe('Bug #2998 (#3004 CR): pristine expansion covers every manifest install
|
||||
assert.equal(written, 1, 'expected agents/ path to be staged and copied to pristine');
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, candidate)), true);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -184,7 +185,7 @@ describe('Bug #2998 (#3004 CR): pristine expansion covers every manifest install
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, a)), true);
|
||||
assert.equal(fs.existsSync(path.join(pristineDir, b)), true);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
@@ -105,7 +106,7 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
|
||||
'begin-phase reset Current Plan to 1 on a mid-flight phase — idempotency guard not applied');
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,7 +124,7 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
|
||||
'begin-phase overwrote stopped_at narrative on a mid-flight phase',
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,7 +143,7 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
|
||||
'begin-phase should set Current Plan to 1 on a fresh phase');
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -172,7 +173,7 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
|
||||
else process.env.GSD_TEST_MODE = origTestMode;
|
||||
if (origNowMs === undefined) delete process.env.GSD_NOW_MS;
|
||||
else process.env.GSD_NOW_MS = origNowMs;
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ const { spawnSync } = require('node:child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
@@ -47,7 +48,7 @@ describe('bug #3212 execute-phase stall detection and safe resume', () => {
|
||||
|
||||
test('config-get returns schema defaults for executor stall detector keys', (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3212-'));
|
||||
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmp));
|
||||
fs.mkdirSync(path.join(tmp, '.planning'));
|
||||
fs.writeFileSync(path.join(tmp, '.planning/config.json'), '{}\n');
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const { parseTomlToObject, validateCodexConfigSchema, install } = require('../bin/install.js');
|
||||
const installModule = require('../bin/install.js');
|
||||
@@ -224,7 +225,7 @@ describe('#3245 — install succeeds with TOML float in pre-existing config', {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install completes when config.toml contains tool_timeout_sec = 20.0', () => {
|
||||
@@ -353,7 +354,7 @@ describe('#3245 — idempotent rollback reverts skills/, agents/, and VERSION',
|
||||
|
||||
afterEach(() => {
|
||||
delete installModule.__codexSchemaValidator;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('validation failure rolls back skills/, agents/, and VERSION to pre-install state', () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ const { execFileSync } = require('child_process');
|
||||
|
||||
const { validateCodexConfigSchema, install } = require('../bin/install.js');
|
||||
const installModule = require('../bin/install.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
if (previousGsdTestMode === undefined) {
|
||||
delete process.env.GSD_TEST_MODE;
|
||||
@@ -219,7 +220,7 @@ describe('#3285 — install succeeds when config.toml contains hooks.state entri
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install does not throw when config.toml contains hooks.state trust entries', () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ const crypto = require('node:crypto');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
const PATCHES_DIR_NAME = 'gsd-local-patches';
|
||||
@@ -63,7 +64,7 @@ describe('Bug #3407: saveLocalPatches preserves old-release pristine across upgr
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.mkdirSync(fakeSrcDir, { recursive: true });
|
||||
t.after(() => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,15 +17,12 @@ const projection = require(path.join(
|
||||
'shell-command-projection.cjs',
|
||||
));
|
||||
const install = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
||||
const { withIsolatedProcessState } = require('./helpers.cjs');
|
||||
const { withIsolatedProcessState, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function createTempHome() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-home-3441-'));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
describe('bug #3441: PATH guidance is projected from typed shell action IR', () => {
|
||||
test('projection module exports PATH action projection helper', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const DRIFT_LINT = path.join(ROOT, 'scripts', 'lint-shell-command-projection-drift.cjs');
|
||||
@@ -44,7 +45,7 @@ describe('bug #3442: shim/wrapper serialized-command drift guard', () => {
|
||||
const result = runLint(fixture);
|
||||
assert.notEqual(result.status, 0, 'inline shim renderer should be rejected by the drift guard');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,7 +65,7 @@ describe('bug #3442: shim/wrapper serialized-command drift guard', () => {
|
||||
const result = runLint(fixture);
|
||||
assert.equal(result.status, 0, `spawnSync/execFileSync should remain allowed:\n${result.stderr || result.stdout}`);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
const { runGsdTools, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// Create a tmpdir whose name always contains a space — this is the invariant
|
||||
// that was violated on /Volumes/Mini Me/... machines.
|
||||
@@ -24,10 +24,6 @@ function createSpacedTmpDir(prefix = 'path with spaces-') {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── dispatcher --cwd= with space in path ────────────────────────────────────
|
||||
|
||||
describe('bug-3509: --cwd= survives spaces in path', () => {
|
||||
|
||||
@@ -27,6 +27,8 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const gsdTools = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
function run(args, cwd) {
|
||||
@@ -159,8 +161,8 @@ function expectParity({ verbWithPadded, verbWithUnpadded, fixtureOpts }) {
|
||||
|
||||
return { aRoadmap, bRoadmap, ra, rb };
|
||||
} finally {
|
||||
fs.rmSync(tmpA, { recursive: true, force: true });
|
||||
fs.rmSync(tmpB, { recursive: true, force: true });
|
||||
cleanup(tmpA);
|
||||
cleanup(tmpB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +209,7 @@ describe('bug #3537: phase verbs accept padded ids against un-padded ROADMAP pro
|
||||
'verb must return non-empty section under both invocations'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -247,7 +249,7 @@ describe('bug #3537: phase verbs accept padded ids against un-padded ROADMAP pro
|
||||
'must not propose 2.1 when 2.7 already exists in ROADMAP'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -298,8 +300,8 @@ describe('bug #3537: phase verbs accept padded ids against un-padded ROADMAP pro
|
||||
// crash mid-run.
|
||||
assert.ok(fs.existsSync(path.join(tmpB, '.planning', 'ROADMAP.md')));
|
||||
} finally {
|
||||
fs.rmSync(tmpA, { recursive: true, force: true });
|
||||
fs.rmSync(tmpB, { recursive: true, force: true });
|
||||
cleanup(tmpA);
|
||||
cleanup(tmpB);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -339,10 +341,10 @@ describe('bug #3537: phase verbs accept padded ids against un-padded ROADMAP pro
|
||||
// and to assert the run did not corrupt the rest of the file.
|
||||
assert.ok(before.length > 0);
|
||||
} finally {
|
||||
fs.rmSync(tmp2, { recursive: true, force: true });
|
||||
cleanup(tmp2);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const EXECUTOR_PATH = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
|
||||
|
||||
@@ -172,6 +173,6 @@ test('bug-3542: stash pushed in main checkout is visible inside a linked worktre
|
||||
'contamination the executor prohibition exists to prevent.',
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
cleanup(tmpRoot);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ const SHARED_DIR = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'shared');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const makeTmpDir = () => createTempDir('gsd-3571-');
|
||||
|
||||
function silenceConsole(fn) {
|
||||
@@ -65,7 +65,7 @@ describe('bug #3571: configuration generated manifests resolve in install layout
|
||||
} else {
|
||||
process.env.GSD_EXPLICIT_CONFIG_DIR = savedExplicitConfigDir;
|
||||
}
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
cleanup(tmpRoot);
|
||||
});
|
||||
|
||||
test('co-located bin/shared manifests let configuration.cjs load without sdk/shared', () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ const ROOT = path.join(__dirname, '..');
|
||||
const { formatGsdSlash, resolveRuntime } = require(
|
||||
path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'runtime-slash.cjs'),
|
||||
);
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
describe('formatGsdSlash — runtime-aware slash command formatter', () => {
|
||||
describe('hyphen-form runtimes (claude, cursor, opencode, kilo, etc.)', () => {
|
||||
@@ -235,7 +236,7 @@ describe('resolveRuntime — env > config > default', () => {
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3584-'));
|
||||
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -256,7 +257,7 @@ describe('resolveRuntime — env > config > default', () => {
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3584-'));
|
||||
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -277,7 +278,7 @@ describe('resolveRuntime — env > config > default', () => {
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3584-'));
|
||||
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -24,6 +24,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const GSD_TOOLS = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
@@ -103,7 +104,7 @@ describe('bug #3631 — SDK family routers forward --raw to output()', () => {
|
||||
`expected next-decimal of base "1" to be 1.1 or 01.1; got: ${trimmed}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,7 +129,7 @@ describe('bug #3631 — SDK family routers forward --raw to output()', () => {
|
||||
`--raw must emit the section body containing the Phase 2 heading; got: ${trimmed.slice(0, 80)}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ const crypto = require('node:crypto');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const cp = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'get-shit-done', 'bin', 'verify-reapply-patches.cjs');
|
||||
@@ -66,7 +67,7 @@ function writeBackupMeta(overrides = {}) {
|
||||
|
||||
function resetFixture() {
|
||||
for (const dir of [patchesDir, configDir, pristineDir]) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
fs.mkdirSync(patchesDir);
|
||||
fs.mkdirSync(configDir);
|
||||
@@ -98,7 +99,7 @@ before(() => {
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
cleanup(tmpRoot);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -57,6 +57,7 @@ const {
|
||||
INSTALL_MIGRATION_LOCK_NAME,
|
||||
runInstallerMigrations,
|
||||
} = require('../get-shit-done/bin/lib/installer-migrations.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -66,10 +67,6 @@ function createTempDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3670-'));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function lockPath(dir) {
|
||||
return path.join(dir, INSTALL_MIGRATION_LOCK_NAME);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
@@ -148,9 +149,7 @@ describe('bug #3683 — command body colon-namespace leak (Claude local install)
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tmpDir) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('E0: staged commands/gsd/ directory exists after install', () => {
|
||||
|
||||
@@ -37,6 +37,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
@@ -135,9 +136,7 @@ describe('bug #3683 — workflow/reference colon-namespace leak (Claude local in
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (claudeTmpDir) {
|
||||
fs.rmSync(claudeTmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
cleanup(claudeTmpDir);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -409,9 +408,7 @@ describe('bug #3683 — workflow/reference colon-namespace leak (Claude local in
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tmpDir) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('G0: staged gemini get-shit-done/workflows/ directory exists after install', () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const os = require('node:os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const HELPER_PATH = path.join(__dirname, '..', 'bin', 'lib', 'ui-safety-gate.cjs');
|
||||
const PLAN_PHASE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'plan-phase.md');
|
||||
@@ -298,7 +299,7 @@ describe('UI gate resolves the helper against RUNTIME_DIR, not the consuming rep
|
||||
assert.strictEqual(res.stdout.trim(), '0',
|
||||
'helper must be found via RUNTIME_DIR and report UI present — not silently no-op');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +309,7 @@ describe('UI gate resolves the helper against RUNTIME_DIR, not the consuming rep
|
||||
const res = runGateFrom(tmp, 'Requirements: backend REST API only');
|
||||
assert.strictEqual(res.stdout.trim(), '1');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -334,8 +335,8 @@ describe('UI gate resolves the helper against RUNTIME_DIR, not the consuming rep
|
||||
assert.strictEqual(res.stdout.trim(), '0',
|
||||
'helper must be found via get-shit-done/bin/lib/ in installed layout and report UI present');
|
||||
} finally {
|
||||
fs.rmSync(fakeRuntime, { recursive: true, force: true });
|
||||
fs.rmSync(consumingProject, { recursive: true, force: true });
|
||||
cleanup(fakeRuntime);
|
||||
cleanup(consumingProject);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
executeWorktreeWaveCleanupPlan,
|
||||
@@ -135,7 +136,7 @@ describe('bug-3707: executeWorktreeWaveCleanupPlan unlocks and retries on locked
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpBase, { recursive: true, force: true });
|
||||
cleanup(tmpBase);
|
||||
});
|
||||
|
||||
test('removes a locked worktree after unlock-retry (real-fs)', () => {
|
||||
@@ -221,7 +222,7 @@ describe('bug-3707: reapOrphanWorktrees', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpBase, { recursive: true, force: true });
|
||||
cleanup(tmpBase);
|
||||
});
|
||||
|
||||
// ── Dead PID + merged branch → reap ────────────────────────────────────────
|
||||
@@ -424,7 +425,7 @@ describe('bug-3707: reapOrphanWorktrees — adversarial edge cases', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpBase, { recursive: true, force: true });
|
||||
cleanup(tmpBase);
|
||||
});
|
||||
|
||||
// ── Gap 1: Non-numeric lock content (real Claude Code format) → ALIVE (fail-closed) ──
|
||||
|
||||
@@ -27,6 +27,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
@@ -129,9 +130,7 @@ describe('bug #376 — Suite 1: Claude install rewrites /gsd: → /gsd- in hook
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tmpDir) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('1a: hooks/ directory is created by the Claude local install', () => {
|
||||
@@ -206,9 +205,7 @@ describe('bug #376 — Suite 2: Cursor install still rewrites /gsd: → /gsd- (r
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tmpDir) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('2a: .cursor/ directory is created by the Cursor local install', () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const TOOLS_PATH = path.join(ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
@@ -234,7 +235,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`record-session overwrote executor-authored Resume File with '${rfMatch[1].trim()}'`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -253,7 +254,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`Expected 'None' to remain when it was already 'None', got: ${rfMatch[1].trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -272,7 +273,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`Expected explicit --resume-file value to be written, got: ${rfMatch[1].trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -293,7 +294,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`advance-plan overwrote executor-authored Status: got '${statusMatch[1].trim()}'`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -318,7 +319,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`Expected phase-complete Status text, got: '${statusMatch[1].trim()}'`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -339,7 +340,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`advance-plan overwrote executor-authored Last Activity: got '${laMatch[1].trim()}'`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -368,7 +369,7 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
|
||||
`advance-plan overwrote executor-authored Current Position Last activity: got '${posActivityMatch[1].trim()}'`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
@@ -109,7 +110,7 @@ describe('Bug #410: finishInstall non-Claude runtime + GSD_TEST_MODE side-effect
|
||||
} finally {
|
||||
// Restore GSD_TEST_MODE and clean up the written file.
|
||||
process.env.GSD_TEST_MODE = saved;
|
||||
try { fs.rmSync(DEFAULTS_PATH); } catch { /* already gone */ }
|
||||
cleanup(DEFAULTS_PATH);
|
||||
try { fs.rmdirSync(GSD_DIR); } catch { /* not empty or already gone */ }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
const { runGsdTools, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('bug #416 case 1: STATE.md v6.0 with only v5.0-phases/ on disk → null
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('validate health emits zero W007 warnings (no prior-milestone phases surfaced)', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
@@ -150,7 +150,7 @@ describe('bug #416 case 2: no STATE.md + multiple archives → version-sort fall
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('validate health succeeds and does not emit W007 for v5.0 archive phases', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
@@ -196,7 +196,7 @@ describe('bug #416 case 3: STATE.md v5.0 with matching v5.0-phases/ → returns
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmpDir); });
|
||||
|
||||
test('validate health emits zero W007 — archive phases are in ROADMAP and active', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
|
||||
@@ -22,6 +22,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const WORKFLOWS_DIR = path.join(__dirname, '..', 'get-shit-done', 'workflows');
|
||||
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
|
||||
@@ -142,8 +143,8 @@ describe('bug-444: resolver finds repo-local .claude install', () => {
|
||||
`Expected stub output "LOCAL_CLAUDE_STUB:ping,test" but got:\n${stdout.trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fakeRoot, { recursive: true, force: true });
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
cleanup(fakeRoot);
|
||||
cleanup(fakeHome);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,8 +205,8 @@ describe('bug-444: resolver finds repo-local .claude install', () => {
|
||||
`Expected $HOME/.claude stub NOT to be invoked, but got:\n${stdout.trim()}`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fakeRoot, { recursive: true, force: true });
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
cleanup(fakeRoot);
|
||||
cleanup(fakeHome);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('bug-474: installer-migrations lock-loop timeout is deterministic via c
|
||||
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-474-lock-'));
|
||||
|
||||
t.after(() => {
|
||||
fs.rmSync(configDir, { recursive: true, force: true });
|
||||
cleanup(configDir);
|
||||
});
|
||||
|
||||
const LOCK_NAME = 'gsd-install-migration.lock';
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const cp = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'scripts', 'changeset', 'cli.cjs');
|
||||
@@ -36,7 +37,7 @@ function runRender(args = []) {
|
||||
}
|
||||
|
||||
before(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-changeset-')); });
|
||||
after(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmp); });
|
||||
|
||||
// Fixtures for extract tests (#3496)
|
||||
// Written as arrays to avoid template-literal indentation injecting
|
||||
@@ -349,7 +350,7 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
|
||||
|
||||
describe('changeset cli render: file-I/O wrapper (#2975)', () => {
|
||||
test('exits 0 with consumed=N when N fragments are folded into CHANGELOG.md and deleted', () => {
|
||||
fs.rmSync(path.join(tmp, '.changeset'), { recursive: true, force: true });
|
||||
cleanup(path.join(tmp, '.changeset'));
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, 'CHANGELOG.md'),
|
||||
'# Changelog\n\n## [Unreleased]\n\n## [1.0.0] - 2026-01-01\n\n### Fixed\n\n- prior fix (#1)\n',
|
||||
|
||||
@@ -13,10 +13,11 @@ const { generateFragmentName, scaffoldFragment, parseFragment } = (() => {
|
||||
const parse = require(path.join(ROOT, 'scripts', 'changeset', 'parse.cjs'));
|
||||
return { ...newCs, parseFragment: parse.parseFragment };
|
||||
})();
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
let tmp;
|
||||
before(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-new-changeset-')); });
|
||||
after(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
after(() => { cleanup(tmp); });
|
||||
|
||||
describe('changeset new: name generator + scaffold writer (#2975)', () => {
|
||||
test('generateFragmentName returns three lowercase words separated by hyphens', () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const CHECK_UPDATE_PATH = path.join(__dirname, '..', 'hooks', 'gsd-check-update.js');
|
||||
|
||||
@@ -60,7 +61,7 @@ describe('detectConfigDir runtime behavior (#1860)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
cleanup(tmpHome);
|
||||
});
|
||||
|
||||
test('returns .claude config dir when both .claude and .config/opencode exist', () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ const os = require('node:os');
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'scripts', 'ci-rebase-check.cjs');
|
||||
const NODE = process.execPath;
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a small inline Node snippet that requires the run() helper
|
||||
@@ -167,7 +168,7 @@ describe('ci-rebase-check: fetch-retry loop resolves when git fetch succeeds', (
|
||||
`Script must not emit "failed after 3 attempts" when fetch succeeded.\nstderr: ${r.stderr}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const assert = require('node:assert/strict');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
@@ -201,7 +202,7 @@ describe('installRuntimeArtifacts (claude global) — skill layout', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('creates correct directory structure skills/gsd-xxx/SKILL.md', () => {
|
||||
@@ -315,7 +316,7 @@ describe('installRuntimeArtifacts path replacement in Claude global skills (#165
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('replaces ~/.claude/ and $HOME/.claude/ paths with absolute configDir prefix on global install', () => {
|
||||
@@ -405,7 +406,7 @@ describe('Legacy commands/gsd/ cleanup', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install removes legacy commands/gsd/ directory when present', () => {
|
||||
@@ -440,7 +441,7 @@ describe('writeManifest tracks skills/ for Claude', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('manifest includes skills/gsd-xxx/SKILL.md entries for Claude runtime', () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// #2153 follow-up: ensure hooks/dist/ exists before any install integration
|
||||
// test runs. The Codex install path copies hook files from hooks/dist/, which
|
||||
@@ -953,7 +954,7 @@ describe('Codex hooks emit: migration produces namespaced AoT so managed-emit co
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-codex-fieldparity-'));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('migration of legacy [hooks.SessionStart] produces two-level nested AoT (#2773)', () => {
|
||||
@@ -1002,7 +1003,7 @@ describe('mergeCodexConfig', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
const sampleBlock = generateCodexConfigBlock([
|
||||
@@ -1321,7 +1322,7 @@ describe('installCodexConfig (integration)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpTarget, { recursive: true, force: true });
|
||||
cleanup(tmpTarget);
|
||||
});
|
||||
|
||||
// Only run if agents/ directory exists (not in CI without full checkout)
|
||||
@@ -1430,7 +1431,7 @@ describe('Codex install hook configuration (e2e)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('Codex install copies hook file that is referenced in hooks.json (#2153)', () => {
|
||||
@@ -2141,7 +2142,7 @@ describe('Codex uninstall symmetry for hook-enabled configs', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('fresh install removes the GSD-added codex_hooks feature on uninstall', () => {
|
||||
@@ -2275,7 +2276,7 @@ describe('Codex uninstall symmetry for hook-enabled configs', () => {
|
||||
const cleaned = stripGsdFromCodexConfig(readCodexConfig(codexHome));
|
||||
assert.strictEqual(cleaned, initialContent, `preserves short-circuited root features assignment: ${initialContent.split('\n')[0]}`);
|
||||
|
||||
fs.rmSync(codexHome, { recursive: true, force: true });
|
||||
cleanup(codexHome);
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const os = require('os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const GSD_TOOLS = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
@@ -29,7 +30,7 @@ describe('config-get --default flag (#1893)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
function run(...args) {
|
||||
|
||||
@@ -877,7 +877,7 @@ describe('Copilot instructions merge/strip', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpMergeDir, { recursive: true, force: true });
|
||||
cleanup(tmpMergeDir);
|
||||
});
|
||||
|
||||
test('creates file from scratch when none exists', () => {
|
||||
@@ -1021,7 +1021,7 @@ describe('Copilot uninstall skill removal', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('identifies gsd-* skill directories for removal', () => {
|
||||
@@ -1083,7 +1083,7 @@ describe('Copilot manifest and patches fixes', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('writeManifest hashes skills for Copilot runtime', () => {
|
||||
@@ -1210,7 +1210,7 @@ describe('E2E: Copilot full install verification', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('installs expected number of skill directories', () => {
|
||||
@@ -1365,7 +1365,7 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('removes engine directory', () => {
|
||||
@@ -1409,7 +1409,7 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
cleanup(td);
|
||||
});
|
||||
|
||||
test('preserves non-GSD content in skills directory', () => {
|
||||
@@ -1470,7 +1470,7 @@ describe('Claude uninstall preserves user-generated files (#1423)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('preserves USER-PROFILE.md across uninstall', () => {
|
||||
|
||||
@@ -1348,7 +1348,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
afterEach(() => {
|
||||
if (worktreeDir) {
|
||||
try { execSyncLocal(`git worktree remove "${worktreeDir}" --force`, { cwd: mainDir, stdio: 'pipe' }); } catch { /* ok */ }
|
||||
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch { /* ok */ }
|
||||
cleanup(worktreeDir);
|
||||
}
|
||||
cleanup(mainDir);
|
||||
});
|
||||
@@ -1359,7 +1359,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
|
||||
// Create a linked worktree
|
||||
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
fs.rmSync(worktreeDir, { recursive: true, force: true });
|
||||
cleanup(worktreeDir);
|
||||
execSyncLocal(`git worktree add "${worktreeDir}" -b test-linked`, { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
// Give the linked worktree its own .planning/
|
||||
@@ -1374,7 +1374,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
test('returns main repo root when linked worktree has no .planning/', () => {
|
||||
// Create a linked worktree (no .planning/ in main or worktree)
|
||||
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
fs.rmSync(worktreeDir, { recursive: true, force: true });
|
||||
cleanup(worktreeDir);
|
||||
execSyncLocal(`git worktree add "${worktreeDir}" -b test-linked-no-plan`, { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
// resolveWorktreeRoot should return the main repo root
|
||||
@@ -1473,7 +1473,7 @@ describe('detectSubRepos', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(projectRoot, { recursive: true, force: true });
|
||||
cleanup(projectRoot);
|
||||
});
|
||||
|
||||
test('returns empty array when no child directories have .git', () => {
|
||||
@@ -1520,7 +1520,7 @@ describe('loadConfig sub_repos auto-sync', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(projectRoot, { recursive: true, force: true });
|
||||
cleanup(projectRoot);
|
||||
});
|
||||
|
||||
test('migrates multiRepo: true to sub_repos array', () => {
|
||||
@@ -1590,7 +1590,7 @@ describe('findProjectRoot', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(projectRoot, { recursive: true, force: true });
|
||||
cleanup(projectRoot);
|
||||
});
|
||||
|
||||
test('returns startDir when no .planning/ exists anywhere', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ const os = require('os');
|
||||
|
||||
const { createHub } = require('../../get-shit-done/bin/lib/command-routing-hub.cjs');
|
||||
const { createDefaultLogger } = require('../../get-shit-done/bin/lib/observability/logger.cjs');
|
||||
const { cleanup } = require('../helpers.cjs');
|
||||
|
||||
// ─── Test fixture setup ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -99,7 +100,7 @@ describe('trace correlation — end-to-end parentTraceId propagation', () => {
|
||||
process.env.GSD_AUDIT = savedAudit;
|
||||
}
|
||||
// Clean up the temp directory
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
// ── Assertions ────────────────────────────────────────────────────────────
|
||||
@@ -210,7 +211,7 @@ describe('trace correlation — end-to-end parentTraceId propagation', () => {
|
||||
} else {
|
||||
process.env.GSD_AUDIT = isolatedSavedAudit;
|
||||
}
|
||||
fs.rmSync(isolatedTmp, { recursive: true, force: true });
|
||||
cleanup(isolatedTmp);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const statusline = require('../hooks/gsd-statusline.js');
|
||||
const { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
@@ -35,7 +36,7 @@ function makeProject({ flag, transcript }) {
|
||||
transcriptPath = path.join(dir, 'transcript.jsonl');
|
||||
fs.writeFileSync(transcriptPath, transcript);
|
||||
}
|
||||
return { dir, transcriptPath, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
||||
return { dir, transcriptPath, cleanup: () => cleanup(dir) };
|
||||
}
|
||||
|
||||
function buildInput(dir, transcriptPath) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const os = require('node:os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const COMMANDS_DIR = path.join(__dirname, '../commands/gsd');
|
||||
const LINT_SCRIPT = path.join(__dirname, '../scripts/lint-descriptions.cjs');
|
||||
@@ -137,7 +138,7 @@ describe('lint-descriptions.cjs', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('rejects a command file with a description over 100 chars', () => {
|
||||
|
||||
@@ -8,6 +8,77 @@ const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { assertWithinAllowlist } = require('../scripts/lib/allowlist-ratchet.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Allowlisted set of user-invocable skills (commands/gsd/*.md, ns-* excluded).
|
||||
// Consolidation target ~58; this set may only SHRINK.
|
||||
// Adding a new skill requires adding it here with justification.
|
||||
// Removing a consolidated skill requires pruning it here.
|
||||
// ---------------------------------------------------------------------------
|
||||
const KNOWN_SKILLS = new Set([
|
||||
'add-tests.md',
|
||||
'ai-integration-phase.md',
|
||||
'audit-fix.md',
|
||||
'audit-milestone.md',
|
||||
'audit-uat.md',
|
||||
'autonomous.md',
|
||||
'capture.md',
|
||||
'cleanup.md',
|
||||
'code-review.md',
|
||||
'complete-milestone.md',
|
||||
'config.md',
|
||||
'debug.md',
|
||||
'discuss-phase.md',
|
||||
'docs-update.md',
|
||||
'eval-review.md',
|
||||
'execute-phase.md',
|
||||
'explore.md',
|
||||
'extract-learnings.md',
|
||||
'fast.md',
|
||||
'forensics.md',
|
||||
'graphify.md',
|
||||
'health.md',
|
||||
'help.md',
|
||||
'import.md',
|
||||
'inbox.md',
|
||||
'ingest-docs.md',
|
||||
'manager.md',
|
||||
'map-codebase.md',
|
||||
'milestone-summary.md',
|
||||
'mvp-phase.md',
|
||||
'new-milestone.md',
|
||||
'new-project.md',
|
||||
'pause-work.md',
|
||||
'phase.md',
|
||||
'plan-phase.md',
|
||||
'plan-review-convergence.md',
|
||||
'pr-branch.md',
|
||||
'profile-user.md',
|
||||
'progress.md',
|
||||
'quick.md',
|
||||
'resume-work.md',
|
||||
'review-backlog.md',
|
||||
'review.md',
|
||||
'secure-phase.md',
|
||||
'settings.md',
|
||||
'ship.md',
|
||||
'sketch.md',
|
||||
'spec-phase.md',
|
||||
'spike.md',
|
||||
'stats.md',
|
||||
'surface.md',
|
||||
'thread.md',
|
||||
'ui-phase.md',
|
||||
'ui-review.md',
|
||||
'ultraplan-phase.md',
|
||||
'undo.md',
|
||||
'update.md',
|
||||
'validate-phase.md',
|
||||
'verify-work.md',
|
||||
'workspace.md',
|
||||
'workstreams.md',
|
||||
]);
|
||||
|
||||
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
@@ -339,22 +410,22 @@ describe('settings.md is kept (merged into config entry point or remains standal
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Group: Skill count reduced
|
||||
// Group: Skill set allowlisted (identity-based, consolidating toward ~58)
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('skill count', () => {
|
||||
test('total user-invocable files in commands/gsd/*.md is <= 63', () => {
|
||||
// Exclude `ns-*.md` namespace meta-skills (#2792) from this cap.
|
||||
describe('skill set', () => {
|
||||
test('user-invocable skill set is allowlisted (consolidating toward ~58)', () => {
|
||||
// Exclude `ns-*.md` namespace meta-skills (#2792) from this guard.
|
||||
// Those are descriptor-only routers selected first by the model and
|
||||
// are not part of the consolidation surface this test tracks; their
|
||||
// own contract is enforced by tests/enh-2792-namespace-skills.test.cjs.
|
||||
const files = fs.readdirSync(COMMANDS_DIR)
|
||||
const currentBasenames = fs.readdirSync(COMMANDS_DIR)
|
||||
.filter((f) => f.endsWith('.md') && !f.startsWith('ns-'));
|
||||
assert.ok(
|
||||
files.length <= 63,
|
||||
[
|
||||
`Expected <= 63 user-invocable skill files, found ${files.length}.`,
|
||||
'Consolidation target is ~58.',
|
||||
].join(' '),
|
||||
);
|
||||
assertWithinAllowlist({
|
||||
label: 'user-invocable skills (commands/gsd)',
|
||||
current: currentBasenames,
|
||||
known: KNOWN_SKILLS,
|
||||
fail: assert.fail,
|
||||
pruneHint: 'edit KNOWN_SKILLS in tests/enh-2790-skill-consolidation.test.cjs',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
/**
|
||||
* eslint-rules.test.cjs
|
||||
*
|
||||
* RuleTester unit tests for the three local ESLint rules:
|
||||
* RuleTester unit tests for the local ESLint rules:
|
||||
* - local/no-source-grep
|
||||
* - local/no-magic-sleep-in-tests
|
||||
* - local/no-elapsed-assertion
|
||||
* - local/no-raw-rmsync-in-tests
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
@@ -16,6 +17,7 @@ const { RuleTester } = require('eslint');
|
||||
const noSourceGrep = require('../eslint-rules/no-source-grep.cjs');
|
||||
const noMagicSleepInTests = require('../eslint-rules/no-magic-sleep-in-tests.cjs');
|
||||
const noElapsedAssertion = require('../eslint-rules/no-elapsed-assertion.cjs');
|
||||
const noRawRmsyncInTests = require('../eslint-rules/no-raw-rmsync-in-tests.cjs');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
@@ -317,3 +319,173 @@ describe('no-elapsed-assertion rule', () => {
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert.equal with timing comparison');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── no-raw-rmsync-in-tests ──────────────────────────────────────────────────
|
||||
|
||||
describe('no-raw-rmsync-in-tests rule', () => {
|
||||
// ── INVALID cases (must error) ────────────────────────────────────────────
|
||||
|
||||
test('invalid: fs.rmSync() in a test file', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noRawRmSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests flags fs.rmSync() in test file');
|
||||
});
|
||||
|
||||
test('invalid: computed member fs["rmSync"]() in a test file', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
fs['rmSync'](d, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noRawRmSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests flags fs["rmSync"]() in test file');
|
||||
});
|
||||
|
||||
test('invalid: destructured rmSync from require("fs") in a test file', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const { rmSync } = require('fs');
|
||||
rmSync(d, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noRawRmSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests flags destructured rmSync from require("fs")');
|
||||
});
|
||||
|
||||
test('invalid: aliased const del = fs.rmSync; del() in a test file', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const del = fs.rmSync;
|
||||
del(d, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noRawRmSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests flags aliased fs.rmSync');
|
||||
});
|
||||
|
||||
test('invalid: allow-test-rule annotation no longer suppresses this rule (Defect 1 fixed)', () => {
|
||||
// A file with // allow-test-rule: <source-grep reason> must still error
|
||||
// on raw rmSync calls. The file-level annotation is for no-source-grep only.
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
const fs = require('fs');
|
||||
fs.rmSync(d, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noRawRmSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests is NOT suppressed by allow-test-rule annotation');
|
||||
});
|
||||
|
||||
// ── VALID cases (must NOT error) ──────────────────────────────────────────
|
||||
|
||||
test('valid: helpers.cleanup() in a test file (no error)', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const { cleanup } = require('../helpers.cjs');
|
||||
cleanup(tmpDir);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests allows helpers.cleanup()');
|
||||
});
|
||||
|
||||
test('valid: bare rmSync() that is NOT fs-derived (local function) is not flagged', () => {
|
||||
// A locally defined function named rmSync must not be flagged — the rule
|
||||
// only tracks names that were bound from require("fs").
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const rmSync = () => {};
|
||||
rmSync(d);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests does not flag a locally-defined rmSync()');
|
||||
});
|
||||
|
||||
// NOTE: The inline `// eslint-disable-next-line local/no-raw-rmsync-in-tests -- reason`
|
||||
// escape hatch is handled entirely by ESLint's own disable-comment mechanism and
|
||||
// cannot be unit-tested here via RuleTester (RuleTester runs the rule under a
|
||||
// different internal namespace so the comment's rule-id doesn't match). The escape
|
||||
// hatch works correctly when ESLint processes real files via `npx eslint`.
|
||||
|
||||
test('valid: fs.rmSync() in a non-test file (rule is inert outside *.test.cjs)', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
`,
|
||||
filename: 'scripts/foo.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests is inert in non-test files');
|
||||
});
|
||||
|
||||
test('valid: member access / assignment without calling (not a CallExpression)', () => {
|
||||
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const orig = fs.rmSync;
|
||||
fs.rmSync = orig;
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-raw-rmsync-in-tests ignores member access / assignment without call');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-update-banner.js');
|
||||
const {
|
||||
@@ -123,7 +124,7 @@ describe('shouldSuppressFailureWarning', () => {
|
||||
);
|
||||
assert.equal(result, false);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -135,7 +136,7 @@ describe('shouldSuppressFailureWarning', () => {
|
||||
const result = shouldSuppressFailureWarning(f, 1000 + RATE_LIMIT_SECONDS - 1);
|
||||
assert.equal(result, true);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -147,7 +148,7 @@ describe('shouldSuppressFailureWarning', () => {
|
||||
const result = shouldSuppressFailureWarning(f, 1000 + RATE_LIMIT_SECONDS + 1);
|
||||
assert.equal(result, false);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -159,7 +160,7 @@ describe('shouldSuppressFailureWarning', () => {
|
||||
const result = shouldSuppressFailureWarning(f, 100);
|
||||
assert.equal(result, false);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -194,7 +195,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
assert.equal(r.status, 0, `expected exit 0, got ${r.status} stderr=${r.stderr}`);
|
||||
assert.equal(r.stdout.trim(), '');
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -213,7 +214,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
assert.ok(parsed.systemMessage.includes('1.40.0'));
|
||||
assert.ok(parsed.systemMessage.includes('/gsd:update'));
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -229,7 +230,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
assert.equal(r.status, 0);
|
||||
assert.equal(r.stdout.trim(), '');
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -243,7 +244,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
assert.equal(typeof parsed.systemMessage, 'string');
|
||||
assert.ok(/check failed/i.test(parsed.systemMessage));
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -272,7 +273,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
'subsequent run within rate-limit window must stay silent'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -284,7 +285,7 @@ describe('gsd-update-banner.js end-to-end', () => {
|
||||
assert.equal(r.status, 0);
|
||||
assert.equal(r.stdout.trim(), '');
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ const {
|
||||
} = require('../get-shit-done/bin/lib/model-profiles.cjs');
|
||||
const { isValidConfigKey } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const makeTmp = (prefix) => createTempDir(`gsd-3023-${prefix}-`);
|
||||
|
||||
function writeConfig(projectDir, config) {
|
||||
@@ -45,7 +45,7 @@ function writeConfig(projectDir, config) {
|
||||
}
|
||||
|
||||
function rmr(p) {
|
||||
try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
cleanup(p);
|
||||
}
|
||||
|
||||
// ─── Schema: AGENT_TO_PHASE_TYPE table + VALID_PHASE_TYPES ──────────────────
|
||||
|
||||
@@ -60,14 +60,14 @@ const {
|
||||
} = require('../get-shit-done/bin/lib/model-profiles.cjs');
|
||||
const { isValidConfigKey } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const makeTmp = (prefix) => createTempDir(`gsd-3024-${prefix}-`);
|
||||
function writeConfig(dir, config) {
|
||||
const planningDir = path.join(dir, '.planning');
|
||||
fs.mkdirSync(planningDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDir, 'config.json'), JSON.stringify(config, null, 2));
|
||||
}
|
||||
function rmr(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ } }
|
||||
function rmr(p) { cleanup(p); }
|
||||
|
||||
// ─── Schema: AGENT_DEFAULT_TIERS coverage + valid tier set ──────────────────
|
||||
|
||||
|
||||
@@ -29,12 +29,17 @@
|
||||
* 8. Every full.md heading is either aliased or in the intentional-orphan allowlist.
|
||||
* 9. The `commands/gsd/help.md` shim passes `$ARGUMENTS` through and advertises
|
||||
* the composable `--brief <topic>` form.
|
||||
*
|
||||
* Tighten-only invariant (issue #597): ceilings track the per-tier high-water mark
|
||||
* within GRACE lines. Budgets may only decrease, never silently creep upward.
|
||||
* The assertTightCeiling() calls below enforce this automatically.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { assertTightCeiling } = require('../scripts/lib/allowlist-ratchet.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const WORKFLOWS = path.join(ROOT, 'get-shit-done', 'workflows');
|
||||
@@ -47,11 +52,25 @@ const MODE_FILES = ['brief.md', 'default.md', 'full.md', 'topic.md'];
|
||||
// "One screen" budgets, including frontmatter/<purpose>/<reference> tags.
|
||||
// These are conservative (one-page conceptual size of ~25 lines of usable
|
||||
// content) but allow for the wrapping tags. Tighten as content stabilizes.
|
||||
//
|
||||
// Ceilings tightened to actualMax + SMALL_GRACE per the ratchet-down rule (#597).
|
||||
// BRIEF ceiling kept at 30 (actualMax=22, slack=8 ≤ SMALL_GRACE=10).
|
||||
const BRIEF_BUDGET = 30;
|
||||
const DEFAULT_BUDGET = 70;
|
||||
// DEFAULT ceiling lowered from 70 → 60 (actualMax=50; #597 ratchet-down).
|
||||
const DEFAULT_BUDGET = 60;
|
||||
// full.md is the LARGE tier (see workflow-size-budget.test.cjs — LARGE_BUDGET = 1500).
|
||||
// The size-budget test is non-recursive so full.md is not covered there; cap it here.
|
||||
const FULL_BUDGET = 1500;
|
||||
// FULL ceiling lowered from 1500 → 844 (actualMax=784; #597 ratchet-down).
|
||||
const FULL_BUDGET = 844;
|
||||
|
||||
// Grace bands:
|
||||
// SMALL_GRACE — for the tiny brief/default/dispatcher files (≤ ~70 lines):
|
||||
// 10 lines of breathing room is proportionate and prevents trivial edits from
|
||||
// failing while still catching any meaningful upward creep.
|
||||
// LARGE_GRACE — for full.md where content fluctuates more:
|
||||
// 60 lines matches the line-budget GRACE used in the other size-budget tests.
|
||||
const SMALL_GRACE = 10;
|
||||
const LARGE_GRACE = 60;
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(file, 'utf8');
|
||||
@@ -71,10 +90,13 @@ describe('feature #3039: tiered help — file structure', () => {
|
||||
});
|
||||
}
|
||||
|
||||
test('dispatcher exists and is small (≤ 40 lines)', () => {
|
||||
// Dispatcher ceiling lowered from 40 → 34 (actualMax=24; #597 ratchet-down).
|
||||
const DISPATCHER_BUDGET = 34;
|
||||
test(`dispatcher exists and is small (≤ ${DISPATCHER_BUDGET} lines)`, () => {
|
||||
assert.ok(fs.existsSync(DISPATCHER));
|
||||
const n = lineCount(DISPATCHER);
|
||||
assert.ok(n <= 40, `dispatcher should be small; got ${n} lines`);
|
||||
assert.ok(n <= DISPATCHER_BUDGET, `dispatcher should be small; got ${n} lines`);
|
||||
assertTightCeiling({ label: 'dispatcher', actualMax: n, ceiling: DISPATCHER_BUDGET, grace: SMALL_GRACE, fail: assert.fail });
|
||||
});
|
||||
|
||||
for (const f of MODE_FILES) {
|
||||
@@ -94,11 +116,13 @@ describe('feature #3039: tiered help — size budgets', () => {
|
||||
test(`brief.md fits one screen (≤ ${BRIEF_BUDGET} lines)`, () => {
|
||||
const n = lineCount(path.join(MODES, 'brief.md'));
|
||||
assert.ok(n <= BRIEF_BUDGET, `brief.md is ${n} lines, budget ${BRIEF_BUDGET}`);
|
||||
assertTightCeiling({ label: 'BRIEF', actualMax: n, ceiling: BRIEF_BUDGET, grace: SMALL_GRACE, fail: assert.fail });
|
||||
});
|
||||
|
||||
test(`default.md fits one screen (≤ ${DEFAULT_BUDGET} lines)`, () => {
|
||||
const n = lineCount(path.join(MODES, 'default.md'));
|
||||
assert.ok(n <= DEFAULT_BUDGET, `default.md is ${n} lines, budget ${DEFAULT_BUDGET}`);
|
||||
assertTightCeiling({ label: 'DEFAULT', actualMax: n, ceiling: DEFAULT_BUDGET, grace: SMALL_GRACE, fail: assert.fail });
|
||||
});
|
||||
|
||||
test('full.md preserves the complete reference (≥ 600 lines)', () => {
|
||||
@@ -113,6 +137,7 @@ describe('feature #3039: tiered help — size budgets', () => {
|
||||
// workflow-size-budget.test.cjs. Cap it here at the LARGE tier limit.
|
||||
const n = lineCount(path.join(MODES, 'full.md'));
|
||||
assert.ok(n <= FULL_BUDGET, `full.md grew to ${n} lines (LARGE budget: ${FULL_BUDGET})`);
|
||||
assertTightCeiling({ label: 'FULL', actualMax: n, ceiling: FULL_BUDGET, grace: LARGE_GRACE, fail: assert.fail });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('feat-3210: fallow integration module', () => {
|
||||
const resolved = resolveFallowBinary({ cwd: tmp, envPath: '' });
|
||||
assert.strictEqual(resolved, fallowPath);
|
||||
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
});
|
||||
|
||||
// H6: replaced wholesale win32 skip with platform-adapted assertion
|
||||
@@ -87,7 +87,7 @@ describe('feat-3210: fallow integration module', () => {
|
||||
'Windows: .cmd candidate must be preferred over bare extensionless file',
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
} else {
|
||||
// H6: non-Windows — non-executable file in PATH must be ignored
|
||||
@@ -101,7 +101,7 @@ describe('feat-3210: fallow integration module', () => {
|
||||
const resolved = resolveFallowBinary({ cwd: tmp, envPath: pathDir });
|
||||
assert.strictEqual(resolved, null);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe('feat-3210: fallow integration module', () => {
|
||||
() => requireFallowBinary({ cwd: tmp, envPath: '' }),
|
||||
/install fallow via `npm install -D fallow` or `cargo install fallow`/,
|
||||
);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
});
|
||||
|
||||
// L3: runFallowAudit against a non-zero-exit binary must surface error state
|
||||
@@ -166,7 +166,7 @@ describe('feat-3210: fallow integration module', () => {
|
||||
'runFallowAudit must return error state (error/exitCode/failed) when binary exits non-zero',
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,7 +267,7 @@ describe('feat-3210: M2 - node_modules/.bin resolution order', () => {
|
||||
const resolved = resolveFallowBinary({ cwd: tmp, envPath: pathDir });
|
||||
assert.strictEqual(resolved, localFallow, 'node_modules/.bin/fallow must win over PATH fallow');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const COMMAND_ALIASES_FILE = path.join(
|
||||
@@ -183,7 +184,7 @@ describe('feat-3251: generated aliases dispatch through real gsd-tools behavior'
|
||||
cli_flag_present: true,
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||
cleanup(projectDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -216,7 +217,7 @@ describe('feat-3251: generated aliases dispatch through real gsd-tools behavior'
|
||||
assert.equal(output.cli_flag_present, false);
|
||||
assert.deepEqual(snapshotProjectState(projectDir), beforeFiles);
|
||||
} finally {
|
||||
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||
cleanup(projectDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,7 +251,7 @@ describe('feat-3251: generated aliases dispatch through real gsd-tools behavior'
|
||||
assert.equal(output.roadmap_mode, null);
|
||||
assert.deepEqual(snapshotProjectState(projectDir), beforeFiles);
|
||||
} finally {
|
||||
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||
cleanup(projectDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -270,7 +271,7 @@ describe('feat-3251: generated aliases dispatch through real gsd-tools behavior'
|
||||
assert.equal(/\n\s*at\s/.test(result.stderr), false, 'non-debug failure must not print a stack trace');
|
||||
assert.deepEqual(snapshotProjectState(projectDir), beforeFiles);
|
||||
} finally {
|
||||
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||
cleanup(projectDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// Helper under test — must exist at this path (GREEN phase wires it up)
|
||||
const scanPhasePlans = require('../get-shit-done/bin/lib/plan-scan.cjs');
|
||||
@@ -44,7 +45,7 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -48,7 +48,7 @@ const {
|
||||
* Create a fresh real-fs scratch dir per test so no two faults share
|
||||
* state. Returns the directory; caller must clean up.
|
||||
*/
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const mkScratch = (name) => createTempDir(`fs-fault-${name}-`);
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ function orphanTmpFiles(dir) {
|
||||
|
||||
test('platformWriteSync happy path writes content atomically (baseline for fault tests)', (t) => {
|
||||
const dir = mkScratch('happy');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'config.json');
|
||||
platformWriteSync(file, '{"k":"v"}\n');
|
||||
assert.equal(fs.readFileSync(file, 'utf-8'), '{"k":"v"}\n');
|
||||
@@ -77,7 +77,7 @@ test('platformWriteSync happy path writes content atomically (baseline for fault
|
||||
|
||||
test('platformWriteSync recovers when renameSync fails (EXDEV cross-device fallback)', (t) => {
|
||||
const dir = mkScratch('exdev');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'config.json');
|
||||
|
||||
// Simulate rename failing once (e.g. cross-device move on a CI runner
|
||||
@@ -104,7 +104,7 @@ test('platformWriteSync recovers when renameSync fails (EXDEV cross-device fallb
|
||||
|
||||
test('platformWriteSync falls back when initial tmp writeFileSync fails (ENOSPC)', (t) => {
|
||||
const dir = mkScratch('enospc');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'config.json');
|
||||
|
||||
// Make the FIRST writeFileSync (to .tmp.<pid>) fail with ENOSPC. The
|
||||
@@ -140,7 +140,7 @@ test('platformWriteSync falls back when initial tmp writeFileSync fails (ENOSPC)
|
||||
|
||||
test('platformWriteSync propagates the FALLBACK error when both tmp and fallback writes fail', (t) => {
|
||||
const dir = mkScratch('double-fail');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'config.json');
|
||||
|
||||
let writeCalls = 0;
|
||||
@@ -178,7 +178,7 @@ test('platformWriteSync propagates the FALLBACK error when both tmp and fallback
|
||||
|
||||
test('platformWriteSync propagates mkdirSync failure unchanged (no swallowed parent-dir errors)', (t) => {
|
||||
const dir = mkScratch('mkdir-fail');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'deep', 'nested', 'config.json');
|
||||
|
||||
const mkdirMock = mock.method(fs, 'mkdirSync', () => {
|
||||
@@ -204,7 +204,7 @@ test('platformWriteSync propagates mkdirSync failure unchanged (no swallowed par
|
||||
|
||||
test('platformWriteSync against a target path that is an existing directory fails cleanly', (t) => {
|
||||
const dir = mkScratch('target-is-dir');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'collides');
|
||||
// Pre-create the target AS a directory so the rename step would
|
||||
// collide with a directory at the destination.
|
||||
@@ -229,7 +229,7 @@ test('platformWriteSync against a target path that is an existing directory fail
|
||||
|
||||
test('platformWriteSync handles paths with spaces, unicode, and newline characters', (t) => {
|
||||
const dir = mkScratch('weird-path');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const cases = [
|
||||
'has spaces in name.json',
|
||||
'unicode-日本語-name.json',
|
||||
@@ -258,7 +258,7 @@ test('platformWriteSync handles paths with spaces, unicode, and newline characte
|
||||
|
||||
test('platformWriteSync never leaks a tmp file after a successful happy-path write', (t) => {
|
||||
const dir = mkScratch('no-orphan-happy');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
for (let i = 0; i < 25; i++) {
|
||||
platformWriteSync(path.join(dir, `f-${i}.json`), `{"i":${i}}\n`);
|
||||
}
|
||||
@@ -274,7 +274,7 @@ test('platformWriteSync never leaks a tmp file after a successful happy-path wri
|
||||
|
||||
test('platformEnsureDir is idempotent on an existing directory', (t) => {
|
||||
const dir = mkScratch('ensure-idem');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const target = path.join(dir, 'a', 'b', 'c');
|
||||
// First call creates; subsequent calls must not throw EEXIST.
|
||||
platformEnsureDir(target);
|
||||
@@ -287,7 +287,7 @@ test('platformEnsureDir is idempotent on an existing directory', (t) => {
|
||||
|
||||
test('platformEnsureDir propagates EACCES when parent dir is unwritable', (t) => {
|
||||
const dir = mkScratch('ensure-fail');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const mkdirMock = mock.method(fs, 'mkdirSync', () => {
|
||||
const err = new Error('EACCES: permission denied');
|
||||
@@ -328,7 +328,7 @@ test('platformWriteSync REPLACES a symlink with a regular file rather than follo
|
||||
}
|
||||
|
||||
const dir = mkScratch('symlink-replace');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
const realTarget = path.join(dir, 'real-target.json');
|
||||
fs.writeFileSync(realTarget, 'original — must not be touched\n');
|
||||
@@ -356,7 +356,7 @@ test('platformWriteSync against a broken symlink replaces it with the intended f
|
||||
return;
|
||||
}
|
||||
const dir = mkScratch('symlink-broken');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const link = path.join(dir, 'dangling.json');
|
||||
fs.symlinkSync(path.join(dir, 'does-not-exist'), link);
|
||||
assert.equal(fs.lstatSync(link).isSymbolicLink(), true, 'pre-check: link is dangling');
|
||||
@@ -380,7 +380,7 @@ test('platformWriteSync survives a concurrent collision on the same target path'
|
||||
// the writer is sync. This exercises mid-flight error recovery, which
|
||||
// is the same concurrency hazard at a lower granularity.)
|
||||
const dir = mkScratch('concurrent');
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(dir));
|
||||
const file = path.join(dir, 'race.json');
|
||||
|
||||
// First write completes normally.
|
||||
|
||||
@@ -28,6 +28,7 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const SOURCE_AGENTS_DIR = path.join(REPO_ROOT, 'agents');
|
||||
@@ -107,7 +108,7 @@ function runGlobalInstall(runtime, tmpHome) {
|
||||
if (prevSkipStale === undefined) delete process.env.GSD_SKIP_STALE_SDK_CHECK;
|
||||
else process.env.GSD_SKIP_STALE_SDK_CHECK = prevSkipStale;
|
||||
// Clean up the isolated HOME dir
|
||||
try { fs.rmSync(isolatedHome, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
||||
cleanup(isolatedHome);
|
||||
}
|
||||
|
||||
return tmpHome;
|
||||
@@ -132,7 +133,7 @@ describe('#443 Claude install: effort: injected into frontmatter', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('gsd-planner.md contains effort: xhigh (heavy tier default)', () => {
|
||||
@@ -170,7 +171,7 @@ describe('#443 Gemini install: effort: absent (Gemini-safe)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('gsd-planner.md does NOT contain effort: (Gemini install)', () => {
|
||||
@@ -201,7 +202,7 @@ describe('#443 Codex install: model_reasoning_effort in .toml (unified resolver)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('gsd-planner.toml contains model_reasoning_effort = "xhigh" (heavy tier)', () => {
|
||||
@@ -255,7 +256,7 @@ describe('#443 Config-driven: effort.agent_overrides drives install-time effort'
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('Claude .md gets effort: low when agent_overrides.gsd-planner=low', () => {
|
||||
@@ -329,7 +330,7 @@ describe('#443 resolveInstallTimeEffort: invalid tokens fall through to valid ef
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
function writeProjectConfig(config) {
|
||||
|
||||
@@ -13,6 +13,8 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const GSD_TOOLS = path.resolve(__dirname, '../get-shit-done/bin/gsd-tools.cjs');
|
||||
|
||||
function runCli(args, env = {}) {
|
||||
@@ -93,7 +95,7 @@ describe('feat-488: effort sync command', () => {
|
||||
// dry-run must not modify the file
|
||||
assert.ok(fs.readFileSync(agentPath, 'utf8').includes('effort: medium'), 'dry-run must not write file');
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('--apply mode rewrites effort: frontmatter to new config value', () => {
|
||||
@@ -115,7 +117,7 @@ describe('feat-488: effort sync command', () => {
|
||||
assert.ok(updated.includes('effort: xhigh'), 'file must be updated to xhigh');
|
||||
assert.ok(!updated.includes('effort: medium'), 'old effort value must be gone');
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('skips agents where effort: already matches config', () => {
|
||||
@@ -134,7 +136,7 @@ describe('feat-488: effort sync command', () => {
|
||||
assert.equal(result.synced, 0, 'nothing to sync when already matching');
|
||||
assert.equal(result.skipped, 1);
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('injects effort: into agent files that lack the frontmatter key', () => {
|
||||
@@ -154,7 +156,7 @@ describe('feat-488: effort sync command', () => {
|
||||
assert.equal(result.changes[0].to, 'max');
|
||||
assert.ok(fs.readFileSync(agentPath, 'utf8').includes('effort: max'), 'effort must be injected');
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('non-claude runtime exits cleanly with informative reason field', () => {
|
||||
@@ -168,7 +170,7 @@ describe('feat-488: effort sync command', () => {
|
||||
assert.ok(result.reason, 'should include a reason message for unsupported runtime');
|
||||
assert.equal(result.synced, 0);
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('home-default effort config gap: applies home-level effort when project config has no effort section', () => {
|
||||
@@ -211,8 +213,8 @@ describe('feat-488: effort sync command', () => {
|
||||
assert.ok(typeof result.synced === 'number', 'synced must be a number');
|
||||
assert.ok(Array.isArray(result.changes), 'changes must be array');
|
||||
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpHome);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('CLI dispatcher: positional args after effort sync are rejected', () => {
|
||||
@@ -245,6 +247,6 @@ describe('feat-488: effort sync command', () => {
|
||||
'CLI --apply must write the updated effort value'
|
||||
);
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ const {
|
||||
const modelCatalog = require('../get-shit-done/bin/lib/model-catalog.cjs');
|
||||
|
||||
const { isValidConfigKey } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
const { createTempDir } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const makeTmp = (prefix) => createTempDir(`gsd-49-${prefix}-`);
|
||||
|
||||
@@ -83,7 +83,7 @@ function writeConfig(dir, config) {
|
||||
}
|
||||
|
||||
function rmr(p) {
|
||||
try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
cleanup(p);
|
||||
}
|
||||
|
||||
// ─── resolveModelPolicy unit tests ──────────────────────────────────────────
|
||||
|
||||
@@ -15,6 +15,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const commandPath = path.join(repoRoot, 'commands', 'gsd', 'forensics.md');
|
||||
@@ -220,7 +221,7 @@ describe('forensics fixture-based tests', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('detects missing artifacts in phase structure', () => {
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('auto-update', () => {
|
||||
head_at_build: 'abcdef0',
|
||||
graphify_version: null,
|
||||
});
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const s = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(s.stale, true, 'auto-build failure must set stale=true');
|
||||
assert.ok(s.last_build_auto_update, 'last_build_auto_update must be exposed');
|
||||
@@ -147,7 +147,7 @@ describe('auto-update', () => {
|
||||
head_at_build: 'abcdef0',
|
||||
graphify_version: null,
|
||||
});
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const s = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(s.stale, true, 'auto-build in-flight must set stale=true');
|
||||
assert.strictEqual(s.last_build_auto_update.status, 'running');
|
||||
@@ -162,7 +162,7 @@ describe('auto-update', () => {
|
||||
head_at_build: 'abcdef0',
|
||||
graphify_version: null,
|
||||
});
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const s = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(s.stale, false, 'fresh graph + ok auto-build => not stale');
|
||||
assert.strictEqual(s.last_build_auto_update.status, 'ok');
|
||||
@@ -170,7 +170,7 @@ describe('auto-update', () => {
|
||||
|
||||
test('graphifyStatus exposes last_build_auto_update: null when status file absent', (t) => {
|
||||
const tmpDir = makeStatusProject(null);
|
||||
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const s = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(s.last_build_auto_update, null);
|
||||
assert.strictEqual(s.stale, false, 'no status file => stale follows mtime only');
|
||||
@@ -316,6 +316,7 @@ describe('auto-update', () => {
|
||||
atomicSleep(50); // yield 50 ms, then re-check (replaces execFileSync('sleep'))
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- best-effort teardown: error is swallowed so cleanup() (which propagates) cannot be used here; a residual temp dir after a detached-subprocess race is harmless (#382)
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
||||
} catch { /* best-effort teardown: a residual temp dir is harmless; never fail the test (#382) */ }
|
||||
}
|
||||
|
||||
@@ -546,9 +546,7 @@ describe('regressions', () => {
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tmpDir) {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('hooks/gsd-graphify-update.sh present at install target', () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
readGsdState,
|
||||
isInstalledAheadOfLatest,
|
||||
} = require('../hooks/gsd-statusline.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ─── parseStateMd ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -396,7 +397,7 @@ describe('todo-resolution: resolves in_progress task from the newest matching to
|
||||
test('resolves in_progress task from the newest matching todos file (#305)', (t) => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-305-'));
|
||||
t.after(() => {
|
||||
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {}
|
||||
cleanup(tempDir);
|
||||
});
|
||||
|
||||
const todosDir = path.join(tempDir, 'todos');
|
||||
|
||||
101
tests/helpers-cleanup.test.cjs
Normal file
101
tests/helpers-cleanup.test.cjs
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* GSD Tools Test Helpers – cleanup() behavioral tests
|
||||
*
|
||||
* Three deterministic, cross-platform tests that verify cleanup()'s
|
||||
* observable contract at the seam rather than probing its internals.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { cleanup, createTempDir } = require('./helpers.cjs');
|
||||
|
||||
// ─── Test 1: Real-FS happy path ──────────────────────────────────────────────
|
||||
|
||||
test('cleanup removes a real temp dir with nested subdirs and files', () => {
|
||||
const dir = createTempDir('gsd-cleanup-test-');
|
||||
const nested = path.join(dir, 'a', 'b', 'c');
|
||||
fs.mkdirSync(nested, { recursive: true });
|
||||
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');
|
||||
fs.writeFileSync(path.join(dir, 'root.txt'), 'world');
|
||||
|
||||
cleanup(dir);
|
||||
|
||||
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should not exist after cleanup');
|
||||
});
|
||||
|
||||
// ─── Test 2: Retry-budget contract ───────────────────────────────────────────
|
||||
|
||||
test('cleanup passes recursive/force/maxRetries/retryDelay options to fs.rmSync', () => {
|
||||
// Use a real temp dir as the target so cleanup() has a valid path argument.
|
||||
// We chdir AWAY from it first so cleanup() does not try to chdir either.
|
||||
const dir = createTempDir('gsd-cleanup-opts-test-');
|
||||
|
||||
// Capture original cwd and shift away from the target.
|
||||
const originalCwd = process.cwd();
|
||||
// Chdir to the parent of the target so cleanup's cwd-guard is a no-op.
|
||||
process.chdir(path.dirname(dir));
|
||||
|
||||
let capturedOptions = null;
|
||||
const realRmSync = fs.rmSync;
|
||||
|
||||
try {
|
||||
// Replace fs.rmSync with a probe that captures options then does nothing.
|
||||
// This is an assignment expression (not a CallExpression) so it satisfies
|
||||
// the ESLint rule that bans raw fs.rmSync(...) call expressions in tests.
|
||||
fs.rmSync = (targetPath, opts) => {
|
||||
capturedOptions = opts;
|
||||
// Do NOT call through — we don't want the dir actually removed here;
|
||||
// we're only testing the options shape.
|
||||
};
|
||||
|
||||
cleanup(dir);
|
||||
} finally {
|
||||
fs.rmSync = realRmSync;
|
||||
process.chdir(originalCwd);
|
||||
// Remove the dir with the real rmSync now that we restored it.
|
||||
cleanup(dir);
|
||||
}
|
||||
|
||||
assert.ok(capturedOptions !== null, 'fs.rmSync should have been called');
|
||||
assert.strictEqual(capturedOptions.recursive, true, 'recursive must be true');
|
||||
assert.strictEqual(capturedOptions.force, true, 'force must be true');
|
||||
assert.ok(
|
||||
typeof capturedOptions.maxRetries === 'number' && capturedOptions.maxRetries > 0,
|
||||
'maxRetries must be a positive number'
|
||||
);
|
||||
assert.ok(
|
||||
typeof capturedOptions.retryDelay === 'number' && capturedOptions.retryDelay > 0,
|
||||
'retryDelay must be a positive number'
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Test 3: cwd-guard ───────────────────────────────────────────────────────
|
||||
|
||||
test('cleanup does not throw when cwd is inside the target dir, and removes the dir', () => {
|
||||
const dir = createTempDir('gsd-cleanup-cwd-test-');
|
||||
const nested = path.join(dir, 'deep', 'nested');
|
||||
fs.mkdirSync(nested, { recursive: true });
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Step INTO the nested subdir so cwd is inside the cleanup target.
|
||||
process.chdir(nested);
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
cleanup(dir);
|
||||
}, 'cleanup should not throw even when cwd is inside the target');
|
||||
} finally {
|
||||
// Restore original cwd. cleanup() will have chdir'd to dirname(dir),
|
||||
// so we always restore explicitly regardless.
|
||||
if (process.cwd() !== originalCwd) {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should not exist after cleanup');
|
||||
});
|
||||
@@ -23,7 +23,7 @@ const {
|
||||
convertClaudeCommandToClaudeSkill,
|
||||
installRuntimeArtifacts,
|
||||
} = require('../bin/install.js');
|
||||
const { parseFrontmatter } = require('./helpers.cjs');
|
||||
const { parseFrontmatter, cleanup } = require('./helpers.cjs');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
const {
|
||||
@@ -138,9 +138,7 @@ describe('Hermes Agent: installRuntimeArtifacts', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(tmpDir)) {
|
||||
fs.rmSync(tmpDir, { recursive: true });
|
||||
}
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('creates skills/gsd/quick/SKILL.md directory structure (Hermes bare-stem layout)', () => {
|
||||
|
||||
@@ -46,6 +46,7 @@ function createTempProject(prefix = 'gsd-hook-test-') {
|
||||
}
|
||||
|
||||
function cleanup(tmpDir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- this IS the local teardown helper; wrapping helpers.cjs cleanup would create a circular dependency
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
|
||||
@@ -203,7 +204,7 @@ describe('opt-in gating behavior', { skip: isWindows ? 'bash hooks require unix
|
||||
test('validate-commit is a no-op when config.json is absent', (t) => {
|
||||
// No config.json at all
|
||||
const bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hook-bare-'));
|
||||
t.after(() => { fs.rmSync(bareDir, { recursive: true, force: true }); });
|
||||
t.after(() => { cleanup(bareDir); });
|
||||
const hookPath = path.join(HOOKS_DIR, 'gsd-validate-commit.sh');
|
||||
const input = JSON.stringify({
|
||||
tool_input: { command: 'git commit -m "WIP save"' }
|
||||
@@ -353,7 +354,7 @@ describe('hook execution when enabled', { skip: isWindows ? 'bash hooks require
|
||||
test('session-state exits 0 without .planning/ (in enabled project)', (t) => {
|
||||
// Create a dir with config but no STATE.md
|
||||
const noStateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hook-nostate-'));
|
||||
t.after(() => { fs.rmSync(noStateDir, { recursive: true, force: true }); });
|
||||
t.after(() => { cleanup(noStateDir); });
|
||||
fs.mkdirSync(path.join(noStateDir, '.planning'), { recursive: true });
|
||||
writeConfigWithHooks(noStateDir, true);
|
||||
const hookPath = path.join(HOOKS_DIR, 'gsd-session-state.sh');
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('install-profiles: stageSkillsForMode', () => {
|
||||
try {
|
||||
assert.strictEqual(stageSkillsForMode(src, 'full'), src);
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -156,8 +156,8 @@ describe('install-profiles: stageSkillsForMode', () => {
|
||||
'phase.md', 'plan-phase.md', 'surface.md', 'update.md'],
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
if (staged) fs.rmSync(staged, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
cleanup(staged);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -170,8 +170,8 @@ describe('install-profiles: stageSkillsForMode', () => {
|
||||
const copied = fs.readFileSync(path.join(staged, 'plan-phase.md'), 'utf8');
|
||||
assert.strictEqual(copied, original);
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
if (staged) fs.rmSync(staged, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
cleanup(staged);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,8 +191,8 @@ describe('install-profiles: stageSkillsForMode', () => {
|
||||
staged = stageSkillsForMode(src, 'minimal');
|
||||
assert.deepStrictEqual(fs.readdirSync(staged), ['plan-phase.md']);
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
if (staged) fs.rmSync(staged, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
cleanup(staged);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -211,7 +211,7 @@ describe('install-profiles: cleanupStagedSkills', () => {
|
||||
assert.ok(!fs.existsSync(a));
|
||||
assert.ok(!fs.existsSync(b));
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -230,7 +230,7 @@ describe('install-profiles: cleanupStagedSkills', () => {
|
||||
const after = process.listenerCount('exit');
|
||||
assert.ok(after - before <= 1, `expected <=1 new exit listener, got ${after - before}`);
|
||||
} finally {
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
cleanupStagedSkills();
|
||||
}
|
||||
});
|
||||
@@ -261,7 +261,7 @@ describe('install-profiles: cleanupStagedSkills', () => {
|
||||
} finally {
|
||||
fs.copyFileSync = realCopy;
|
||||
fs.mkdtempSync = realMkdtemp;
|
||||
fs.rmSync(src, { recursive: true, force: true });
|
||||
cleanup(src);
|
||||
cleanupStagedSkills();
|
||||
}
|
||||
});
|
||||
@@ -296,7 +296,7 @@ describe('install: --minimal honoured for every runtime in --global mode', () =>
|
||||
);
|
||||
assert.strictEqual(manifestAgentCount(manifest), 0);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -315,7 +315,7 @@ describe('install: --minimal honoured for every runtime in --local mode', () =>
|
||||
);
|
||||
assert.strictEqual(manifestAgentCount(manifest), 0);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -333,7 +333,7 @@ describe('install: Cline --minimal (rules-based, no skills/ dir)', () => {
|
||||
assert.strictEqual(manifestAgentCount(manifest), 0);
|
||||
assert.ok(fs.existsSync(path.join(configDir, '.clinerules')));
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -358,7 +358,7 @@ describe('install: on-disk skill files match manifest for --minimal', () => {
|
||||
assert.deepStrictEqual(gsdAgents, []);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -385,7 +385,7 @@ describe('install: manifest records mode for both profiles', () => {
|
||||
const agentCount = Object.keys(m.files || {}).filter(k => k.startsWith('agents/')).length;
|
||||
return { mode: m.mode, skillCount, agentCount };
|
||||
} finally {
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
cleanup(targetDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ describe('install-minimal-backcompat: --minimal and --profile=core produce same
|
||||
const profileMarker = fs.existsSync(markerPath) ? fs.readFileSync(markerPath, 'utf8').trim() : null;
|
||||
return { mode: m.mode, skillCount, profileMarker };
|
||||
} finally {
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
cleanup(targetDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ describe('install: Codex full → minimal downgrade cleans stale agent state', (
|
||||
}
|
||||
assert.ok(fs.existsSync(configPath));
|
||||
} finally {
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
cleanup(targetDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -545,7 +545,7 @@ describe('install: Claude full → minimal downgrade removes stale agents', () =
|
||||
assert.ok(remaining.includes('my-custom-agent.md'));
|
||||
assert.deepStrictEqual(remaining.filter(f => f.startsWith('gsd-')), []);
|
||||
} finally {
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
cleanup(targetDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ function createTempHome() {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup predates helpers.cjs; name collision prevents import
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
resolveEffectiveProfile,
|
||||
mostRestrictiveProfile,
|
||||
@@ -36,7 +38,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: null, targetDir: dir });
|
||||
assert.strictEqual(result, 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,7 +49,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: null, targetDir: dir });
|
||||
assert.strictEqual(result, 'standard');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,7 +60,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: null, targetDir: dir });
|
||||
assert.strictEqual(result, 'core');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +71,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: 'full', targetDir: dir });
|
||||
assert.strictEqual(result, 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -80,7 +82,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: 'standard', targetDir: dir });
|
||||
assert.strictEqual(result, 'standard');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,7 +93,7 @@ describe('resolveEffectiveProfile', () => {
|
||||
const result = resolveEffectiveProfile({ requestedProfileName: null, targetDir: dir });
|
||||
assert.strictEqual(result, 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -159,7 +161,7 @@ describe('marker-driven profile resolution end-to-end', () => {
|
||||
if (staged) cleanupStagedSkills();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,8 +177,8 @@ describe('marker-driven profile resolution end-to-end', () => {
|
||||
assert.strictEqual(resolved, 'core',
|
||||
'core is smaller than standard — most-restrictive wins');
|
||||
} finally {
|
||||
fs.rmSync(dirA, { recursive: true, force: true });
|
||||
fs.rmSync(dirB, { recursive: true, force: true });
|
||||
cleanup(dirA);
|
||||
cleanup(dirB);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -190,7 +192,7 @@ describe('marker-driven profile resolution end-to-end', () => {
|
||||
const resolved = resolveProfile({ modes: [effective], manifest });
|
||||
assert.strictEqual(resolved.skills, '*', 'full profile should be sentinel');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ function createTempInstall() {
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup predates helpers.cjs; name collision prevents import
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ function isolateHome() {
|
||||
function restoreHome() {
|
||||
if (_origHome === undefined) delete process.env.HOME; else process.env.HOME = _origHome;
|
||||
if (_origGsdHome === undefined) delete process.env.GSD_HOME; else process.env.GSD_HOME = _origGsdHome;
|
||||
if (_isolatedHome) fs.rmSync(_isolatedHome, { recursive: true, force: true });
|
||||
cleanup(_isolatedHome);
|
||||
_isolatedHome = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { installCodexConfig } = require('../bin/install.js');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
function makeTempDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2639-'));
|
||||
@@ -54,7 +55,7 @@ describe('#2639 — Codex TOML emit routes through full neutralization pipeline'
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('strips CLAUDE.md, .claude/skills/, .claude/commands/, .claude/agents/, and .claudeignore from emitted TOML', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ const { execFileSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const GSD_TOOLS = path.join(ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { resolveUpdateContext } = require(
|
||||
path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'update-context.cjs'),
|
||||
);
|
||||
@@ -137,7 +138,7 @@ describe('gsd-tools update-context (CLI): emits the JSON contract', () => {
|
||||
assert.equal(ctx.scope, 'GLOBAL');
|
||||
assert.equal(ctx.runtime, 'kilo');
|
||||
} finally {
|
||||
nodeFs.rmSync(tmp, { recursive: true, force: true });
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,9 +37,7 @@ function makeTempDir() {
|
||||
* @param {string} dir
|
||||
*/
|
||||
function cleanupDir(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
cleanup(dir);
|
||||
}
|
||||
|
||||
// ─── Write ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -6,6 +6,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
evaluateLint,
|
||||
@@ -284,7 +285,7 @@ describe('docs-required lint: readFragmentsFromDisk', () => {
|
||||
fs.mkdirSync(path.join(tmp, '.changeset'), { recursive: true });
|
||||
fn(tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 20, retryDelay: 250 });
|
||||
cleanup(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user