build(#188): retire sdk generator and freshness scripts

This commit is contained in:
Tom Boucher
2026-05-25 09:41:37 -04:00
parent 140c3ed76d
commit 4c92aacca5
43 changed files with 1 additions and 5269 deletions

View File

@@ -1,355 +0,0 @@
'use strict';
/**
* Parity test: configuration.generated.cjs (CJS) vs sdk/dist/config/index.js (ESM).
*
* For every fixture in the vitest pinning tests, asserts that both sides produce
* identical output. This ensures the generator faithfully replicates the TS source.
*
* Uses node:test + dynamic import() for the ESM side.
*/
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
// ─── CJS side (synchronous require) ──────────────────────────────────────────
const cjs = require('../get-shit-done/bin/lib/configuration.generated.cjs');
// ─── Helpers ──────────────────────────────────────────────────────────────────
function makeTmpProject() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-'));
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
return dir;
}
function writeConfig(dir, data) {
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify(data, null, 2));
}
function readConfigRaw(dir) {
return fs.readFileSync(path.join(dir, '.planning', 'config.json'), 'utf-8');
}
function cleanup(dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
// ─── ESM side (loaded once via before()) ─────────────────────────────────────
let esm;
before(async () => {
esm = await import('../sdk/dist/config/index.js');
});
// ─── Parity helper ────────────────────────────────────────────────────────────
/**
* Deep-equal assertion that normalizes Sets to arrays for comparison.
*/
function assertDeepEqual(label, actual, expected) {
const normalize = (v) => JSON.parse(JSON.stringify(v, (_k, val) =>
val instanceof Set ? [...val].sort() : val
));
assert.deepStrictEqual(normalize(actual), normalize(expected), `${label} mismatch`);
}
// ─── CONFIG_DEFAULTS parity ───────────────────────────────────────────────────
describe('CONFIG_DEFAULTS parity', () => {
test('model_profile matches', () => {
assert.strictEqual(cjs.CONFIG_DEFAULTS.model_profile, esm.CONFIG_DEFAULTS.model_profile);
});
test('git section matches', () => {
assertDeepEqual('git', cjs.CONFIG_DEFAULTS.git, esm.CONFIG_DEFAULTS.git);
});
test('workflow section matches', () => {
assertDeepEqual('workflow', cjs.CONFIG_DEFAULTS.workflow, esm.CONFIG_DEFAULTS.workflow);
});
test('hooks section matches', () => {
assertDeepEqual('hooks', cjs.CONFIG_DEFAULTS.hooks, esm.CONFIG_DEFAULTS.hooks);
});
});
// ─── VALID_CONFIG_KEYS parity ─────────────────────────────────────────────────
describe('VALID_CONFIG_KEYS parity', () => {
test('same size', () => {
assert.strictEqual(cjs.VALID_CONFIG_KEYS.size, esm.VALID_CONFIG_KEYS.size);
});
test('same entries', () => {
for (const key of esm.VALID_CONFIG_KEYS) {
assert.ok(cjs.VALID_CONFIG_KEYS.has(key), `CJS missing key: ${key}`);
}
for (const key of cjs.VALID_CONFIG_KEYS) {
assert.ok(esm.VALID_CONFIG_KEYS.has(key), `ESM missing key: ${key}`);
}
});
});
// ─── DYNAMIC_KEY_PATTERNS parity ─────────────────────────────────────────────
describe('DYNAMIC_KEY_PATTERNS parity', () => {
test('same length', () => {
assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS.length, esm.DYNAMIC_KEY_PATTERNS.length);
});
test('same topLevel and source strings', () => {
for (let i = 0; i < esm.DYNAMIC_KEY_PATTERNS.length; i++) {
assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS[i].topLevel, esm.DYNAMIC_KEY_PATTERNS[i].topLevel, `topLevel[${i}]`);
assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS[i].source, esm.DYNAMIC_KEY_PATTERNS[i].source, `source[${i}]`);
}
});
test('test functions produce same results', () => {
const sampleKeys = [
'agent_skills.planner',
'agent_skills.executor',
'review.models.ollama',
'features.thinking_partner',
'claude_md_assembly.blocks.intro',
'model_profile_overrides.openai.opus',
'models.planning',
'dynamic_routing.enabled',
'model_overrides.my-agent',
'workflow.research',
'unknown_key',
];
for (const key of sampleKeys) {
for (let i = 0; i < esm.DYNAMIC_KEY_PATTERNS.length; i++) {
const esmResult = esm.DYNAMIC_KEY_PATTERNS[i].test(key);
const cjsResult = cjs.DYNAMIC_KEY_PATTERNS[i].test(key);
assert.strictEqual(cjsResult, esmResult, `pattern[${i}].test('${key}')`);
}
}
});
});
// ─── normalizeLegacyKeys parity ───────────────────────────────────────────────
describe('normalizeLegacyKeys parity', () => {
test('branching_strategy migration', () => {
const input = { branching_strategy: 'phase' };
const esmR = esm.normalizeLegacyKeys(input);
const cjsR = cjs.normalizeLegacyKeys(input);
assertDeepEqual('parsed', cjsR.parsed, esmR.parsed);
assertDeepEqual('normalizations', cjsR.normalizations, esmR.normalizations);
});
test('sub_repos migration', () => {
const input = { sub_repos: ['app1', 'app2'] };
const esmR = esm.normalizeLegacyKeys(input);
const cjsR = cjs.normalizeLegacyKeys(input);
assertDeepEqual('parsed', cjsR.parsed, esmR.parsed);
assertDeepEqual('normalizations', cjsR.normalizations, esmR.normalizations);
});
test('multiRepo migration', () => {
const input = { multiRepo: true };
const esmR = esm.normalizeLegacyKeys(input);
const cjsR = cjs.normalizeLegacyKeys(input);
assertDeepEqual('parsed', cjsR.parsed, esmR.parsed);
assert.strictEqual(cjsR.normalizations.length, esmR.normalizations.length);
assert.strictEqual(cjsR.normalizations[0].requiresFilesystem, esmR.normalizations[0].requiresFilesystem);
});
test('depth: comprehensive migration', () => {
const input = { depth: 'comprehensive' };
const esmR = esm.normalizeLegacyKeys(input);
const cjsR = cjs.normalizeLegacyKeys(input);
assertDeepEqual('parsed', cjsR.parsed, esmR.parsed);
});
test('already-normalized returns empty normalizations', () => {
const input = { git: { branching_strategy: 'phase' } };
const esmR = esm.normalizeLegacyKeys(input);
const cjsR = cjs.normalizeLegacyKeys(input);
assert.strictEqual(cjsR.normalizations.length, 0);
assert.strictEqual(esmR.normalizations.length, 0);
});
test('idempotent — second call returns empty normalizations', () => {
const input = { branching_strategy: 'milestone' };
const first_cjs = cjs.normalizeLegacyKeys(input);
const second_cjs = cjs.normalizeLegacyKeys(first_cjs.parsed);
const first_esm = esm.normalizeLegacyKeys(input);
const second_esm = esm.normalizeLegacyKeys(first_esm.parsed);
assert.strictEqual(second_cjs.normalizations.length, 0);
assert.strictEqual(second_esm.normalizations.length, 0);
assertDeepEqual('second_parsed', second_cjs.parsed, second_esm.parsed);
});
});
// ─── mergeDefaults parity ─────────────────────────────────────────────────────
describe('mergeDefaults parity', () => {
test('empty input returns CONFIG_DEFAULTS shape', () => {
const esmR = esm.mergeDefaults({});
const cjsR = cjs.mergeDefaults({});
assert.strictEqual(cjsR.model_profile, esmR.model_profile);
assertDeepEqual('git', cjsR.git, esmR.git);
assertDeepEqual('workflow', cjsR.workflow, esmR.workflow);
assertDeepEqual('hooks', cjsR.hooks, esmR.hooks);
});
test('partial nested preserves siblings', () => {
const input = { git: { base_branch: 'main' } };
const esmR = esm.mergeDefaults(input);
const cjsR = cjs.mergeDefaults(input);
assert.strictEqual(cjsR.git.base_branch, esmR.git.base_branch);
assert.strictEqual(cjsR.git.branching_strategy, esmR.git.branching_strategy);
});
test('boolean false preserved', () => {
const input = { workflow: { research: false } };
const esmR = esm.mergeDefaults(input);
const cjsR = cjs.mergeDefaults(input);
assert.strictEqual(cjsR.workflow.research, false);
assert.strictEqual(esmR.workflow.research, false);
});
test('null preserved', () => {
const input = { project_code: null };
const esmR = esm.mergeDefaults(input);
const cjsR = cjs.mergeDefaults(input);
assert.strictEqual(cjsR.project_code, null);
assert.strictEqual(esmR.project_code, null);
});
});
// ─── loadConfig parity ────────────────────────────────────────────────────────
describe('loadConfig parity', () => {
test('missing config.json returns defaults', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-lc-'));
try {
const esmR = await esm.loadConfig(dir);
const cjsR = await cjs.loadConfig(dir);
assert.strictEqual(cjsR.model_profile, esmR.model_profile);
assertDeepEqual('git', cjsR.git, esmR.git);
} finally {
cleanup(dir);
}
});
test('empty {} config.json returns defaults', async () => {
const dir = makeTmpProject();
writeConfig(dir, {});
try {
const esmR = await esm.loadConfig(dir);
const cjsR = await cjs.loadConfig(dir);
assert.strictEqual(cjsR.model_profile, esmR.model_profile);
} finally {
cleanup(dir);
}
});
test('nested git.branching_strategy preserved', async () => {
const dir = makeTmpProject();
writeConfig(dir, { git: { branching_strategy: 'phase' } });
try {
const esmR = await esm.loadConfig(dir);
const cjsR = await cjs.loadConfig(dir);
assert.strictEqual(cjsR.git.branching_strategy, 'phase');
assert.strictEqual(esmR.git.branching_strategy, 'phase');
} finally {
cleanup(dir);
}
});
test('legacy top-level branching_strategy normalized, disk unchanged', async () => {
const dir = makeTmpProject();
writeConfig(dir, { branching_strategy: 'phase' });
const before = readConfigRaw(dir);
try {
const esmR = await esm.loadConfig(dir);
const cjsR = await cjs.loadConfig(dir);
assert.strictEqual(cjsR.git.branching_strategy, 'phase');
assert.strictEqual(esmR.git.branching_strategy, 'phase');
// Disk must be unchanged
assert.strictEqual(readConfigRaw(dir), before);
} finally {
cleanup(dir);
}
});
test('throws on malformed JSON', async () => {
const dir = makeTmpProject();
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{bad json');
try {
await assert.rejects(() => cjs.loadConfig(dir), /parse|invalid|json/i);
await assert.rejects(() => esm.loadConfig(dir), /parse|invalid|json/i);
} finally {
cleanup(dir);
}
});
});
// ─── migrateOnDisk parity ─────────────────────────────────────────────────────
describe('migrateOnDisk parity', () => {
test('no-op for already-normalized config', async () => {
const dir = makeTmpProject();
writeConfig(dir, { git: { branching_strategy: 'phase' } });
try {
const esmR = await esm.migrateOnDisk(dir);
// Reset file for CJS test
writeConfig(dir, { git: { branching_strategy: 'phase' } });
const cjsR = await cjs.migrateOnDisk(dir);
assert.strictEqual(cjsR.migrated, false);
assert.strictEqual(esmR.migrated, false);
assert.strictEqual(cjsR.wrote, null);
assert.strictEqual(esmR.wrote, null);
} finally {
cleanup(dir);
}
});
test('migrates legacy key and writes disk', async () => {
const dirEsm = makeTmpProject();
const dirCjs = makeTmpProject();
writeConfig(dirEsm, { branching_strategy: 'phase' });
writeConfig(dirCjs, { branching_strategy: 'phase' });
try {
const esmR = await esm.migrateOnDisk(dirEsm);
const cjsR = await cjs.migrateOnDisk(dirCjs);
assert.strictEqual(cjsR.migrated, true);
assert.strictEqual(esmR.migrated, true);
assert.ok(cjsR.wrote !== null);
assert.ok(esmR.wrote !== null);
// Both should have normalized the disk file
const cjsDisk = JSON.parse(readConfigRaw(dirCjs));
const esmDisk = JSON.parse(readConfigRaw(dirEsm));
assert.strictEqual(cjsDisk.branching_strategy, undefined);
assert.strictEqual(esmDisk.branching_strategy, undefined);
assert.strictEqual(cjsDisk.git?.branching_strategy, 'phase');
assert.strictEqual(esmDisk.git?.branching_strategy, 'phase');
} finally {
cleanup(dirEsm);
cleanup(dirCjs);
}
});
test('missing file returns migrated:false, wrote:null', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-md-'));
try {
const esmR = await esm.migrateOnDisk(dir);
const cjsR = await cjs.migrateOnDisk(dir);
assert.strictEqual(cjsR.migrated, false);
assert.strictEqual(esmR.migrated, false);
assert.strictEqual(cjsR.wrote, null);
assert.strictEqual(esmR.wrote, null);
} finally {
cleanup(dir);
}
});
});

View File

@@ -1,217 +0,0 @@
'use strict';
/**
* Parity test: decisions.generated.cjs vs sdk/src/query/decisions.ts
*
* Verifies that the generated CJS artifact matches the SDK source-of-truth
* for all supported ID formats (numeric and alphanumeric) and edge cases.
*
* Covers: Phase 6 (#3575) MIGRATE_ME resolution for decisions.cjs.
*/
const assert = require('assert');
const { describe, test } = require('node:test');
const { parseDecisions } = require('../get-shit-done/bin/lib/decisions.cjs');
// ─── Core parity: numeric IDs (legacy format) ────────────────────────────────
describe('decisions-generator parity — numeric IDs (legacy)', () => {
test('extracts D-NN entries with {id, text}', () => {
const md = `
<decisions>
## Implementation Decisions
### Auth
- **D-01:** Use OAuth 2.0 with PKCE
- **D-02:** Session storage in Redis
### Storage
- **D-03:** Postgres 15 with pgvector
</decisions>
`;
const ds = parseDecisions(md);
assert.deepStrictEqual(ds.map(d => d.id), ['D-01', 'D-02', 'D-03']);
assert.strictEqual(ds[0].text, 'Use OAuth 2.0 with PKCE');
});
test('returns [] when no <decisions> block is present', () => {
assert.deepStrictEqual(parseDecisions('# Just a header\nno decisions here'), []);
});
test('returns [] for empty / null / undefined input', () => {
assert.deepStrictEqual(parseDecisions(''), []);
assert.deepStrictEqual(parseDecisions(null), []);
assert.deepStrictEqual(parseDecisions(undefined), []);
});
test('ignores D-IDs outside the <decisions> block', () => {
const md = `
Top of file. - **D-99:** Not a real decision (outside block).
<decisions>
- **D-01:** Real decision
</decisions>
After the block. - **D-77:** Also not real.
`;
const ds = parseDecisions(md);
assert.deepStrictEqual(ds.map(d => d.id), ['D-01']);
});
});
// ─── Phase 6 extension: alphanumeric IDs ─────────────────────────────────────
describe('decisions-generator parity — alphanumeric IDs (Phase 6 extension)', () => {
test('accepts alphanumeric IDs: D-INFRA-01', () => {
const md = `
<decisions>
### Infrastructure
- **D-INFRA-01:** Use Kubernetes for orchestration
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds.length, 1);
assert.strictEqual(ds[0].id, 'D-INFRA-01');
assert.strictEqual(ds[0].text, 'Use Kubernetes for orchestration');
});
test('accepts alphanumeric IDs: D-42 (single numeric)', () => {
const md = `
<decisions>
### Architecture
- **D-42:** Use microservices
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds[0].id, 'D-42');
});
test('accepts mixed numeric and alphanumeric IDs in same block', () => {
const md = `
<decisions>
### Planning
- **D-01:** First numeric decision
- **D-FOO_BAR:** Alphanumeric with underscore
- **D-ARCH-123:** Mixed alphanumeric with hyphen
</decisions>
`;
const ds = parseDecisions(md);
const ids = ds.map(d => d.id);
assert.ok(ids.includes('D-01'), 'should have D-01');
assert.ok(ids.includes('D-FOO_BAR'), 'should have D-FOO_BAR');
assert.ok(ids.includes('D-ARCH-123'), 'should have D-ARCH-123');
});
test('CJS callers can use {id, text} shape — extra fields present but safe to ignore', () => {
const md = `
<decisions>
### Category
- **D-INFRA-01:** Database selection
</decisions>
`;
const ds = parseDecisions(md);
const d = ds[0];
// Verify {id, text} is present as CJS callers expect
assert.strictEqual(typeof d.id, 'string');
assert.strictEqual(typeof d.text, 'string');
// Extra SDK fields are present but can be ignored
assert.ok('category' in d, 'category field present');
assert.ok('tags' in d, 'tags field present');
assert.ok('trackable' in d, 'trackable field present');
});
});
// ─── Richer schema fields (SDK extension) ────────────────────────────────────
describe('decisions-generator parity — richer schema', () => {
test('marks decisions under "Claude\'s Discretion" as non-trackable', () => {
const md = `
<decisions>
### Claude's Discretion
- **D-50:** Internal naming is flexible
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds[0].trackable, false);
});
test('marks [informational] tagged decisions as non-trackable', () => {
const md = `
<decisions>
### Info
- **D-03 [informational]:** Background context only
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds[0].trackable, false);
assert.ok(ds[0].tags.includes('informational'));
});
test('marks [folded] tagged decisions as non-trackable', () => {
const md = `
<decisions>
### Deferred
- **D-05 [folded]:** Will handle later
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds[0].trackable, false);
});
test('extracts category from ### heading', () => {
const md = `
<decisions>
### Storage Backend
- **D-01:** Use PostgreSQL
</decisions>
`;
const ds = parseDecisions(md);
assert.strictEqual(ds[0].category, 'Storage Backend');
});
test('parses ALL <decisions> blocks (not just first)', () => {
const md = `
<decisions>
### One
- **D-01:** First batch
</decisions>
Some prose.
<decisions>
### Two
- **D-02:** Second batch
</decisions>
`;
const ids = parseDecisions(md).map(d => d.id);
assert.ok(ids.includes('D-01'));
assert.ok(ids.includes('D-02'));
});
test('strips fenced code blocks before parsing', () => {
const md = `
\`\`\`
<decisions>
### Fake
- **D-99:** Should not be parsed
</decisions>
\`\`\`
<decisions>
### Real
- **D-01:** Real decision
</decisions>
`;
const ds = parseDecisions(md);
const ids = ds.map(d => d.id);
assert.ok(ids.includes('D-01'));
assert.ok(!ids.includes('D-99'));
});
test('curly-quote "Claude’s Discretion" variant is non-trackable', () => {
const content =
'<decisions>\n### Claude’s Discretion\n- **D-50:** Should be non-trackable\n</decisions>';
const ds = parseDecisions(content);
const d50 = ds.find(d => d.id === 'D-50');
assert.ok(d50, 'D-50 should be found');
assert.strictEqual(d50.trackable, false);
});
});

View File

@@ -1,351 +0,0 @@
'use strict';
/**
* Generator correctness, parity, and atomicity (#3598).
*
* Existing per-generator tests (configuration-generator.test.cjs et al.)
* assert positive-path behavior and CJS/ESM API parity for one or two
* generators each. None of them assert:
*
* 1. The committed `.generated.cjs` is byte-equal to a fresh re-run
* of the generator's `build*Cjs()` export (stale-but-timestamp-valid
* detection — issue #3598 AC #4).
* 2. The generator is deterministic — two back-to-back calls return
* identical strings (AC #1, defends against time/random/env-order
* sneaking into output).
* 3. The runtime CJS alias surface and the SDK TS alias surface
* expose the same canonical/alias set (AC #3, beyond timestamp
* freshness).
* 4. The live command registry contains no duplicate aliases
* (AC #1: "duplicate aliases" — translated to a behavioral
* structural invariant on the in-memory registry the generator
* reads from, since the generator has no fixture/--source seam).
* 5. `build-hooks.js` is idempotent (running twice leaves `hooks/dist/`
* byte-identical and clears its per-PID staging directory — proves
* the atomic-write seam does not leak partial artifacts; AC #2).
*
* This suite fills those gaps without duplicating any happy-path
* coverage that already exists.
*/
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { spawnSync } = require('node:child_process');
const REPO_ROOT = path.resolve(__dirname, '..');
// ─── Helpers ────────────────────────────────────────────────────────────────
const sdkScriptUrl = (script) =>
new URL(`file://${path.join(REPO_ROOT, 'sdk', 'scripts', script)}`).href;
function sha256(buf) {
return crypto.createHash('sha256').update(buf).digest('hex');
}
function normalizeEol(text) {
return String(text).replace(/\r\n/g, '\n');
}
/** Read a directory recursively into a Map<relPath, sha256>. */
function snapshotDir(dir) {
const out = new Map();
if (!fs.existsSync(dir)) return out;
const walk = (sub) => {
for (const e of fs.readdirSync(sub, { withFileTypes: true })) {
const full = path.join(sub, e.name);
if (e.isDirectory()) walk(full);
else if (e.isFile()) out.set(path.relative(dir, full), sha256(fs.readFileSync(full)));
}
};
walk(dir);
return out;
}
// ─── Suite 1: build*Cjs() === committed file (stale-detection) ──────────────
// Each row: { script, exportName, committed }
// `committed` is the path the generator writes to. The generator's exported
// build*Cjs() must produce a string equal to fs.readFileSync(committed). If
// the committed file has drifted (manual edit, partial generator run,
// stale-but-timestamp-valid), this assertion fails — which is the
// behavioral coverage the issue's AC #4 calls for.
const CJS_GENERATORS = [
{ script: 'gen-configuration.mjs', exportName: 'buildConfigurationCjs', committed: 'get-shit-done/bin/lib/configuration.generated.cjs' },
{ script: 'gen-project-root.mjs', exportName: 'buildProjectRootCjs', committed: 'get-shit-done/bin/lib/project-root.generated.cjs' },
{ script: 'gen-state-document.ts', exportName: 'buildStateDocumentCjs', committed: 'get-shit-done/bin/lib/state-document.generated.cjs' },
{ script: 'gen-workstream-inventory-builder.mjs', exportName: 'buildWorkstreamInventoryBuilderCjs', committed: 'get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs' },
{ script: 'gen-decisions.mjs', exportName: 'buildDecisionsCjs', committed: 'get-shit-done/bin/lib/decisions.generated.cjs' },
{ script: 'gen-plan-scan.mjs', exportName: 'buildPlanScanCjs', committed: 'get-shit-done/bin/lib/plan-scan.generated.cjs' },
{ script: 'gen-schema-detect.mjs', exportName: 'buildSchemaDetectCjs', committed: 'get-shit-done/bin/lib/schema-detect.generated.cjs' },
{ script: 'gen-secrets.mjs', exportName: 'buildSecretsCjs', committed: 'get-shit-done/bin/lib/secrets.generated.cjs' },
{ script: 'gen-workstream-name-policy.mjs', exportName: 'buildWorkstreamNamePolicyCjs', committed: 'get-shit-done/bin/lib/workstream-name-policy.generated.cjs' },
];
describe('feat-3598: build*Cjs() output matches committed .generated.cjs (stale-detection)', () => {
for (const g of CJS_GENERATORS) {
test(`${g.script} → ${path.basename(g.committed)} is fresh`, async () => {
// `.ts` generators need ts-node/loader to import directly from .cjs
// tests. Skip them here — their fresh-check covers the same property
// via the `check-*-fresh.mjs` subprocess pathway, which runs in CI.
if (g.script.endsWith('.ts')) {
return; // covered by sdk/scripts/check-state-document-fresh.mjs
}
const mod = await import(sdkScriptUrl(g.script));
const builder = mod[g.exportName];
assert.equal(typeof builder, 'function',
`${g.script} must export ${g.exportName} as a function`);
const fresh = await builder();
assert.equal(typeof fresh, 'string', `${g.exportName}() must return a string`);
const committedPath = path.join(REPO_ROOT, g.committed);
const committed = fs.readFileSync(committedPath, 'utf-8');
assert.equal(normalizeEol(fresh), normalizeEol(committed),
`${g.committed} drifted from generator output — run "cd sdk && npm run gen:${g.script.replace(/^gen-/, '').replace(/\.mjs$/, '')}" to regenerate`);
});
}
});
// ─── Suite 2: generators are deterministic ──────────────────────────────────
describe('feat-3598: build*Cjs() is deterministic across calls', () => {
for (const g of CJS_GENERATORS) {
test(`${g.exportName} produces identical output on back-to-back calls`, async () => {
if (g.script.endsWith('.ts')) return; // see suite 1 note
const mod = await import(sdkScriptUrl(g.script));
const builder = mod[g.exportName];
const a = await builder();
const b = await builder();
assert.equal(a, b,
`${g.exportName}() output differed between two calls — generator has non-deterministic input ` +
`(time, random, env-order, Map/Set iteration)`);
});
}
});
// ─── Suite 3: runtime CJS ↔ SDK TS alias parity ─────────────────────────────
describe('feat-3598: command-aliases CJS and TS surfaces expose the same alias set', () => {
const CJS_PATH = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs');
const TS_PATH = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-aliases.generated.ts');
test('both files exist', () => {
assert.ok(fs.existsSync(CJS_PATH), `missing ${CJS_PATH}`);
assert.ok(fs.existsSync(TS_PATH), `missing ${TS_PATH}`);
});
test('canonical command names are identical between runtimes', () => {
const cjs = require(CJS_PATH);
const cjsArrays = [
'STATE_COMMAND_ALIASES',
'VERIFY_COMMAND_ALIASES',
'INIT_COMMAND_ALIASES',
'PHASE_COMMAND_ALIASES',
'PHASES_COMMAND_ALIASES',
'VALIDATE_COMMAND_ALIASES',
'ROADMAP_COMMAND_ALIASES',
'NON_FAMILY_COMMAND_ALIASES',
];
const cjsCanonicals = new Set();
for (const key of cjsArrays) {
assert.ok(Array.isArray(cjs[key]), `CJS export ${key} must be an array`);
for (const e of cjs[key]) cjsCanonicals.add(e.canonical);
}
// The TS file is the same data emitted as a TS source. Read it and
// extract canonical strings via a structural pattern (`canonical: '...'`)
// restricted to the generated-file format the generator emits — never
// a free-form text scan. This satisfies the CONTRIBUTING typed-IR
// requirement because the source file *is* the generator's output:
// its lexical shape is part of the deployed contract.
// allow-test-rule: source-text-is-the-product
const ts = fs.readFileSync(TS_PATH, 'utf-8');
const tsCanonicals = new Set(
[...ts.matchAll(/canonical:\s*'([^']+)'/g)].map((m) => m[1]),
);
assert.deepEqual(
[...tsCanonicals].sort(),
[...cjsCanonicals].sort(),
'TS and CJS surfaces must expose the same canonical command set — regenerate via "cd sdk && npm run gen:command-aliases"',
);
});
test('alias strings are identical between runtimes (set equality)', () => {
const cjs = require(CJS_PATH);
const cjsAliases = new Set();
for (const key of [
'STATE_COMMAND_ALIASES',
'VERIFY_COMMAND_ALIASES',
'INIT_COMMAND_ALIASES',
'PHASE_COMMAND_ALIASES',
'PHASES_COMMAND_ALIASES',
'VALIDATE_COMMAND_ALIASES',
'ROADMAP_COMMAND_ALIASES',
'NON_FAMILY_COMMAND_ALIASES',
]) {
for (const e of cjs[key]) for (const a of e.aliases || []) cjsAliases.add(a);
}
// allow-test-rule: source-text-is-the-product
const ts = fs.readFileSync(TS_PATH, 'utf-8');
// Aliases are emitted as: aliases: ['foo', 'bar', 'baz']
// Match the literal-array bodies, then extract individual quoted strings.
const tsAliases = new Set();
for (const m of ts.matchAll(/aliases:\s*\[([^\]]*)\]/g)) {
for (const a of m[1].matchAll(/'([^']+)'/g)) tsAliases.add(a[1]);
}
assert.deepEqual(
[...tsAliases].sort(),
[...cjsAliases].sort(),
'TS and CJS alias sets must be identical — regenerate via "cd sdk && npm run gen:command-aliases"',
);
});
});
// ─── Suite 4: no duplicate aliases in the live registry ─────────────────────
describe('feat-3598: live command registry has no duplicate aliases', () => {
// This is the behavioral equivalent of the issue's example test
// "generator rejects duplicate command aliases with actionable error".
// The generator has no fixture seam — it reads in-memory
// COMMAND_DEFINITIONS_BY_FAMILY. The structural invariant that the
// generator MUST emit a registry with no duplicates is what we assert
// here, on the deployed surface. If two definitions ever collide,
// this test fails with the colliding alias named — actionable in the
// same way the issue's example error would be.
test('no alias appears twice across all command families', () => {
const cjs = require(path.join(
REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs',
));
const seen = new Map(); // alias → canonical
const collisions = [];
for (const key of [
'STATE_COMMAND_ALIASES',
'VERIFY_COMMAND_ALIASES',
'INIT_COMMAND_ALIASES',
'PHASE_COMMAND_ALIASES',
'PHASES_COMMAND_ALIASES',
'VALIDATE_COMMAND_ALIASES',
'ROADMAP_COMMAND_ALIASES',
'NON_FAMILY_COMMAND_ALIASES',
]) {
for (const e of cjs[key]) {
for (const a of e.aliases || []) {
if (seen.has(a)) {
collisions.push(`alias "${a}" claimed by both "${seen.get(a)}" and "${e.canonical}"`);
} else {
seen.set(a, e.canonical);
}
}
}
}
assert.equal(collisions.length, 0,
`duplicate aliases in command registry:\n ${collisions.join('\n ')}`);
});
test('no canonical command appears twice', () => {
const cjs = require(path.join(
REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs',
));
const seen = new Set();
const duplicates = [];
for (const key of [
'STATE_COMMAND_ALIASES',
'VERIFY_COMMAND_ALIASES',
'INIT_COMMAND_ALIASES',
'PHASE_COMMAND_ALIASES',
'PHASES_COMMAND_ALIASES',
'VALIDATE_COMMAND_ALIASES',
'ROADMAP_COMMAND_ALIASES',
'NON_FAMILY_COMMAND_ALIASES',
]) {
for (const e of cjs[key]) {
if (seen.has(e.canonical)) duplicates.push(e.canonical);
seen.add(e.canonical);
}
}
assert.deepEqual(duplicates, [],
`canonical command appears in more than one family: ${duplicates.join(', ')}`);
});
});
// ─── Suite 5: build-hooks atomicity (idempotence + no orphan staging) ───────
describe('feat-3598: build-hooks.js is idempotent and leaves no staging residue', () => {
const HOOKS_DIR = path.join(REPO_ROOT, 'hooks');
const DIST_DIR = path.join(HOOKS_DIR, 'dist');
// A baseline snapshot taken once before either run. The "fresh dist" the
// first run produces is compared to the second run's dist. We do NOT
// compare against the disk state before the first run, because the
// baseline dist on disk could itself be stale at the time the suite
// happens to run (e.g. a fresh clone with an old build artifact).
let snapshotA;
let stagingBefore;
before(() => {
// Run build:hooks once to land a known-fresh dist.
const r1 = spawnSync(process.execPath, [path.join('scripts', 'build-hooks.js')], {
cwd: REPO_ROOT,
encoding: 'utf-8',
timeout: 60000,
});
assert.equal(r1.status, 0,
`build-hooks first run must exit 0; stderr=${r1.stderr.slice(0, 400)}`);
snapshotA = snapshotDir(DIST_DIR);
assert.ok(snapshotA.size > 0, 'build-hooks must produce at least one file in hooks/dist/');
stagingBefore = fs.readdirSync(HOOKS_DIR)
.filter((n) => n.startsWith('.dist-staging-'));
});
test('second run produces byte-identical hooks/dist/ contents', () => {
const r2 = spawnSync(process.execPath, [path.join('scripts', 'build-hooks.js')], {
cwd: REPO_ROOT,
encoding: 'utf-8',
timeout: 60000,
});
assert.equal(r2.status, 0,
`build-hooks second run must exit 0; stderr=${r2.stderr.slice(0, 400)}`);
const snapshotB = snapshotDir(DIST_DIR);
assert.equal(snapshotB.size, snapshotA.size,
`hooks/dist/ file count must be stable: a=${snapshotA.size} b=${snapshotB.size}`);
for (const [rel, hashA] of snapshotA) {
const hashB = snapshotB.get(rel);
assert.equal(hashB, hashA,
`hooks/dist/${rel} changed between consecutive runs (atomic-write must produce identical output)`);
}
});
test('no orphaned .dist-staging-* directories remain after the run', () => {
// The second run (just above) creates and cleans its own staging dir.
// Any leftover with the second-run's PID would prove the cleanup
// step failed. We can only check the *current* state — that is, any
// staging directory that was not present before the build started.
const stagingAfter = fs.readdirSync(HOOKS_DIR)
.filter((n) => n.startsWith('.dist-staging-'));
const orphaned = stagingAfter.filter((n) => !stagingBefore.includes(n));
assert.deepEqual(orphaned, [],
`build-hooks left orphaned staging directories: ${orphaned.join(', ')}`);
});
test('every file in hooks/dist/ that is JavaScript parses without SyntaxError', () => {
// Positive proof of the build-hooks syntax guard: every shipped .js
// file must parse. If a SyntaxError survives the guard and lands in
// dist/, this assertion fails with the file named.
const vm = require('node:vm');
for (const [rel] of snapshotDir(DIST_DIR)) {
if (!rel.endsWith('.js')) continue;
const src = fs.readFileSync(path.join(DIST_DIR, rel), 'utf-8');
assert.doesNotThrow(
() => new vm.Script(src, { filename: rel }),
`hooks/dist/${rel} has a SyntaxError — build-hooks syntax guard let it through`,
);
}
});
});

View File

@@ -1,201 +0,0 @@
'use strict';
/**
* Regression tests for the requireFreshDist() staleness guard in gen-*.mjs scripts.
*
* For each generator, verifies:
* 1. Exits 1 when sdk/dist file does not exist — error includes "does not exist"
* and the `npm run build:sdk` hint.
* 2. Exits 1 when TS source is newer than sdk/dist — error includes
* "is stale relative to", both mtime timestamps, the TS path, and build hint.
* 3. Exits 0 when sdk/dist is newer than TS source (fresh build).
* Skipped per-generator if dist doesn't exist (expected before first build:sdk).
*
* Uses child_process.spawnSync to exercise the real gen-*.mjs entry path.
*
* Isolation: each subtest creates its own temp directory rooted at a unique
* path and passes GSD_REPO_ROOT to the generator subprocess so requireFreshDist()
* operates on temp fixtures instead of the real sdk/dist tree. This prevents
* parallel test execution from seeing stale/missing dist files in the live tree.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const REPO_ROOT = path.resolve(__dirname, '..');
const SCRIPTS_DIR = path.join(REPO_ROOT, 'sdk', 'scripts');
// Map of generator script → { dist, ts } repo-relative paths
const GENERATORS = [
{
script: 'gen-plan-scan.mjs',
dist: 'sdk/dist/query/plan-scan.js',
ts: 'sdk/src/query/plan-scan.ts',
},
{
script: 'gen-secrets.mjs',
dist: 'sdk/dist/query/secrets.js',
ts: 'sdk/src/query/secrets.ts',
},
{
script: 'gen-schema-detect.mjs',
dist: 'sdk/dist/query/schema-detect.js',
ts: 'sdk/src/query/schema-detect.ts',
},
{
script: 'gen-decisions.mjs',
dist: 'sdk/dist/query/decisions.js',
ts: 'sdk/src/query/decisions.ts',
},
{
script: 'gen-project-root.mjs',
dist: 'sdk/dist/runtime/project-root.js',
ts: 'sdk/src/runtime/project-root.ts',
},
{
script: 'gen-workstream-inventory-builder.mjs',
dist: 'sdk/dist/workstream/builder.js',
ts: 'sdk/src/workstream/builder.ts',
},
{
script: 'gen-workstream-name-policy.mjs',
dist: 'sdk/dist/workstream-name-policy.js',
ts: 'sdk/src/workstream-name-policy.ts',
},
{
script: 'gen-validate.mjs',
dist: 'sdk/dist/query/validate.js',
ts: 'sdk/src/query/validate.ts',
},
{
script: 'gen-configuration.mjs',
dist: 'sdk/dist/config/index.js',
ts: 'sdk/src/config/index.ts',
},
];
/**
* Run a gen script via spawnSync with an optional GSD_REPO_ROOT override.
* Returns { status, stderr, stdout }.
*/
function runGen(scriptName, env = {}) {
const scriptPath = path.join(SCRIPTS_DIR, scriptName);
const result = spawnSync(process.execPath, [scriptPath], {
cwd: REPO_ROOT,
encoding: 'utf-8',
timeout: 15000,
env: { ...process.env, ...env },
});
return {
status: result.status ?? 1,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
/**
* Create a minimal temp directory tree that mirrors the repo layout for
* the given dist and ts paths. Returns { tmpRoot, distAbs, tsAbs, cleanup }.
*
* The real TS source is copied into the temp tree so its content is valid,
* but the caller controls whether the dist file exists and what its mtime is.
*/
function makeTempTree(dist, ts) {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-staleness-'));
const distAbs = path.join(tmpRoot, dist);
const tsAbs = path.join(tmpRoot, ts);
// Always create the TS source (copy from real tree so content is valid)
fs.mkdirSync(path.dirname(tsAbs), { recursive: true });
const realTsAbs = path.join(REPO_ROOT, ts);
fs.copyFileSync(realTsAbs, tsAbs);
function cleanup() {
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
}
return { tmpRoot, distAbs, tsAbs, cleanup };
}
// One describe block per generator — each block is { concurrency: false } so
// its three subtests run in order and don't race on the same files.
for (const { script, dist, ts } of GENERATORS) {
describe(`gen staleness guard — ${script}`, { concurrency: false }, () => {
// ── Subtest A: dist file is missing ──────────────────────────────────────
test('exits 1 with "does not exist" when dist file is absent', () => {
const { tmpRoot, cleanup } = makeTempTree(dist, ts);
// dist is NOT created — the temp tree has only the TS source
try {
const { status, stderr } = runGen(script, { GSD_REPO_ROOT: tmpRoot });
assert.strictEqual(status, 1, `Expected exit 1, got ${status}. stderr: ${stderr}`);
assert.match(stderr, /does not exist/, `Expected "does not exist". Got: ${stderr}`);
assert.match(stderr, /npm run build/, `Expected build hint. Got: ${stderr}`);
// Error message should name the dist path (repo-relative)
assert.ok(stderr.includes(dist), `Expected dist path in message. Got: ${stderr}`);
} finally {
cleanup();
}
});
// ── Subtest B: TS source newer than dist ─────────────────────────────────
test('exits 1 with stale-dist error when TS source is newer than dist', () => {
const { tmpRoot, distAbs, tsAbs, cleanup } = makeTempTree(dist, ts);
// Create a fake dist file
fs.mkdirSync(path.dirname(distAbs), { recursive: true });
fs.writeFileSync(distAbs, '// fake dist for staleness test\n', 'utf-8');
// Set dist mtime 2s in the past, ts mtime to now → ts is newer
const past = new Date(Date.now() - 2000);
const now = new Date();
fs.utimesSync(distAbs, past, past);
fs.utimesSync(tsAbs, now, now);
try {
const { status, stderr } = runGen(script, { GSD_REPO_ROOT: tmpRoot });
assert.strictEqual(status, 1, `Expected exit 1 for stale dist, got ${status}. stderr: ${stderr}`);
assert.match(stderr, /is stale relative to/, `Expected "is stale relative to". Got: ${stderr}`);
assert.match(stderr, /npm run build/, `Expected build hint. Got: ${stderr}`);
// Error must name the TS source path
assert.ok(stderr.includes(ts), `Expected TS path "${ts}" in message. Got: ${stderr}`);
// Error must include both mtime timestamps in ISO format
assert.match(stderr, /dist mtime \d{4}-\d{2}-\d{2}T/, `Expected dist mtime in message. Got: ${stderr}`);
assert.match(stderr, /ts mtime \d{4}-\d{2}-\d{2}T/, `Expected ts mtime in message. Got: ${stderr}`);
} finally {
cleanup();
}
});
// ── Subtest C: dist is fresh → exits 0 ───────────────────────────────────
// This subtest requires a real built dist (the generator reads and transforms
// its content), so it uses the actual sdk/dist tree rather than the temp dir.
// It only mutates the real TS source mtime, not the dist file, so it is safe
// to run in parallel: another parallel test process reading sdk/dist will not
// be affected by a TS source mtime change.
test('exits 0 when dist is newer than TS source', { skip: !fs.existsSync(path.join(REPO_ROOT, dist)) ? `${dist} not built` : false }, () => {
const distAbs = path.join(REPO_ROOT, dist);
const tsAbs = path.join(REPO_ROOT, ts);
const distStat = fs.statSync(distAbs);
const origTsStat = fs.statSync(tsAbs);
// Set ts mtime to 2s before dist mtime so dist is definitely newer
const tsOlderThanDist = new Date(distStat.mtimeMs - 2000);
fs.utimesSync(tsAbs, tsOlderThanDist, tsOlderThanDist);
try {
const { status, stderr, stdout } = runGen(script);
assert.strictEqual(
status,
0,
`Expected exit 0 for fresh dist, got ${status}. stderr: ${stderr}\nstdout: ${stdout}`,
);
} finally {
fs.utimesSync(tsAbs, origTsStat.atime, origTsStat.mtime);
}
});
});
}

View File

@@ -1,196 +0,0 @@
'use strict';
/**
* Parity test — verifies that plan-scan.generated.cjs produces identical
* results to the compiled SDK ESM output for all exported functions.
*
* SDK side: import('../sdk/dist/query/plan-scan.js')
* CJS side: require('../get-shit-done/bin/lib/plan-scan.generated.cjs')
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { createRequire } = require('node:module');
const path = require('path');
const os = require('os');
const fs = require('fs');
const crypto = require('crypto');
/**
* Build a unique-to-this-run path that is guaranteed not to exist. Hardcoded
* `/tmp/...` paths are a flake source on shared CI runners where the path can
* be left over from a prior run. We synthesize a random suffix under
* `os.tmpdir()` and force-remove the path first.
*/
function uniqueMissingPath(prefix = 'gsd-missing') {
const suffix = `${prefix}-${process.pid}-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`;
const p = path.join(os.tmpdir(), suffix);
// The probability of collision is negligible, but force-clean anyway to make
// the precondition explicit. Errors swallowed (path didn't exist — desired).
try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ }
return p;
}
const requireFromRoot = createRequire(__filename);
// CJS side — direct require works fine
const cjs = requireFromRoot('../get-shit-done/bin/lib/plan-scan.generated.cjs');
// ── isRootPlanFile ────────────────────────────────────────────────────────
describe('plan-scan-generator parity: isRootPlanFile', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
const fixtures = [
{ label: 'accepts bare PLAN.md', name: 'PLAN.md', expected: true },
{ label: 'accepts canonical -PLAN.md', name: '01-01-PLAN.md', expected: true },
{ label: 'accepts extended PLAN-01-setup.md', name: 'PLAN-01-setup.md', expected: true },
{ label: 'rejects -PLAN-OUTLINE.md', name: 'something-PLAN-OUTLINE.md', expected: false },
{ label: 'rejects .pre-bounce.md', name: 'PLAN.pre-bounce.md', expected: false },
{ label: 'rejects SUMMARY.md', name: 'SUMMARY.md', expected: false },
{ label: 'rejects unrelated file', name: 'README.md', expected: false },
];
for (const { label, name, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.isRootPlanFile(name);
const cjsResult = cjs.isRootPlanFile(name);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── isNestedPlanFile ──────────────────────────────────────────────────────
describe('plan-scan-generator parity: isNestedPlanFile', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
const fixtures = [
{ label: 'accepts PLAN-01-setup.md', name: 'PLAN-01-setup.md', expected: true },
{ label: 'accepts 1-PLAN-01-setup.md', name: '1-PLAN-01-setup.md', expected: true },
{ label: 'rejects PLAN-OUTLINE.md', name: 'PLAN-01-OUTLINE.md', expected: false },
{ label: 'rejects .pre-bounce.md', name: 'PLAN-01.pre-bounce.md', expected: false },
{ label: 'rejects bare PLAN.md', name: 'PLAN.md', expected: false },
{ label: 'rejects unrelated file', name: 'SUMMARY-01-setup.md', expected: false },
];
for (const { label, name, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.isNestedPlanFile(name);
const cjsResult = cjs.isNestedPlanFile(name);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── isRootSummaryFile ─────────────────────────────────────────────────────
describe('plan-scan-generator parity: isRootSummaryFile', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
const fixtures = [
{ label: 'accepts bare SUMMARY.md', name: 'SUMMARY.md', expected: true },
{ label: 'accepts 01-01-SUMMARY.md', name: '01-01-SUMMARY.md', expected: true },
{ label: 'rejects PLAN.md', name: 'PLAN.md', expected: false },
{ label: 'rejects unrelated file', name: 'README.md', expected: false },
];
for (const { label, name, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.isRootSummaryFile(name);
const cjsResult = cjs.isRootSummaryFile(name);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── isNestedSummaryFile ───────────────────────────────────────────────────
describe('plan-scan-generator parity: isNestedSummaryFile', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
const fixtures = [
{ label: 'accepts SUMMARY-01-summary.md', name: 'SUMMARY-01-summary.md', expected: true },
{ label: 'accepts 1-SUMMARY-01.md', name: '1-SUMMARY-01.md', expected: true },
{ label: 'rejects bare SUMMARY.md', name: 'SUMMARY.md', expected: false },
{ label: 'rejects PLAN file', name: 'PLAN-01-setup.md', expected: false },
];
for (const { label, name, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.isNestedSummaryFile(name);
const cjsResult = cjs.isNestedSummaryFile(name);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── scanPhasePlans ────────────────────────────────────────────────────────
describe('plan-scan-generator parity: scanPhasePlans (non-existent dir)', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
test('returns zero counts for non-existent directory', () => {
const nonExistent = uniqueMissingPath('gsd-plan-scan-nonexistent');
const sdkResult = sdk.scanPhasePlans(nonExistent);
const cjsResult = cjs.scanPhasePlans(nonExistent);
assert.deepStrictEqual(sdkResult, {
planCount: 0,
summaryCount: 0,
completed: false,
hasNestedPlans: false,
planFiles: [],
summaryFiles: [],
});
assert.deepStrictEqual(sdkResult, cjsResult, 'SDK/CJS parity: non-existent dir');
});
});
describe('plan-scan-generator parity: scanPhasePlans (flat layout)', async () => {
const sdk = await import('../sdk/dist/query/plan-scan.js');
test('detects flat plan and summary files', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-plan-scan-test-'));
try {
fs.writeFileSync(path.join(tmpDir, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(tmpDir, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(tmpDir, 'README.md'), '# Readme');
const sdkResult = sdk.scanPhasePlans(tmpDir);
const cjsResult = cjs.scanPhasePlans(tmpDir);
assert.strictEqual(sdkResult.planCount, 1, 'SDK: planCount');
assert.strictEqual(sdkResult.summaryCount, 1, 'SDK: summaryCount');
assert.strictEqual(sdkResult.completed, true, 'SDK: completed');
assert.strictEqual(sdkResult.hasNestedPlans, false, 'SDK: hasNestedPlans');
assert.deepStrictEqual(sdkResult, cjsResult, 'SDK/CJS parity: flat layout');
} finally {
fs.rmSync(tmpDir, { recursive: true });
}
});
});
describe('plan-scan-generator parity: module.exports call style', async () => {
test('default export is callable as function (CJS caller pattern)', () => {
// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs')
// then call it directly: scanPhasePlans(phaseDir)
assert.strictEqual(typeof cjs, 'function', 'default export is a function');
const result = cjs(uniqueMissingPath('gsd-plan-scan-cjs-default'));
assert.deepStrictEqual(result, {
planCount: 0,
summaryCount: 0,
completed: false,
hasNestedPlans: false,
planFiles: [],
summaryFiles: [],
});
});
});

View File

@@ -1,152 +0,0 @@
'use strict';
/**
* CJS parity test — project-root module
*
* For every fixture from sdk/src/runtime/project-root.test.ts, asserts that
* both the SDK (ESM, via dynamic import) and the generated CJS artifact
* return identical paths. This confirms that the generator correctly
* captures the function body and that all dependencies (sep, dirname,
* relative, etc.) are properly shimmed in the CJS preamble.
*/
const { describe, it, before, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// CJS artifact — synchronous require
const { findProjectRoot: findProjectRootCjs } = require('../get-shit-done/bin/lib/project-root.generated.cjs');
// SDK ESM — loaded once before all tests via dynamic import
let findProjectRootSdk;
before(async () => {
const mod = await import('../sdk/dist/runtime/project-root.js');
findProjectRootSdk = mod.findProjectRoot;
});
// ── Fixture helpers ─────────────────────────────────────────────────────────
const { createTempDir } = require('./helpers.cjs');
const makeTmp = () => createTempDir('gsd-parity-');
function writeConfig(dir, content) {
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify(content));
}
function assertParity(startDir) {
const sdkResult = findProjectRootSdk(startDir);
const cjsResult = findProjectRootCjs(startDir);
assert.strictEqual(
cjsResult,
sdkResult,
`parity failure for startDir="${startDir}": SDK="${sdkResult}" CJS="${cjsResult}"`,
);
return sdkResult;
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe('project-root CJS/SDK parity', () => {
let workspace;
before(() => {
workspace = makeTmp();
});
afterEach(() => {
// Clean the workspace tree and recreate fresh for next test
try { fs.rmSync(workspace, { recursive: true, force: true }); } catch {}
workspace = makeTmp();
});
it('heuristic 0: startDir has own .planning/ — returns startDir', () => {
fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true });
assertParity(workspace);
});
it('no ancestor .planning/ — returns startDir', () => {
assertParity(workspace);
});
it('heuristic 1: parent .planning/config.json lists child in sub_repos', () => {
writeConfig(workspace, { sub_repos: ['child'] });
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.git'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, workspace);
});
it('heuristic 1 nested: deeply nested dir inside sub_repo', () => {
writeConfig(workspace, { sub_repos: ['child'] });
const nested = path.join(workspace, 'child', 'src', 'utils');
fs.mkdirSync(path.join(workspace, 'child', '.git'), { recursive: true });
fs.mkdirSync(nested, { recursive: true });
const result = assertParity(nested);
assert.strictEqual(result, workspace);
});
it('heuristic 1 nested key: planning.sub_repos config shape', () => {
writeConfig(workspace, { planning: { sub_repos: ['child'] } });
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.git'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, workspace);
});
it('heuristic 2: multiRepo: true with .git in ancestor chain', () => {
writeConfig(workspace, { multiRepo: true });
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.git'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, workspace);
});
it('heuristic 3: parent has .planning/ and child has .git, no config', () => {
fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true });
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.git'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, workspace);
});
it('malformed config.json falls back to heuristic 3', () => {
fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true });
fs.writeFileSync(path.join(workspace, '.planning', 'config.json'), '{ not json');
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.git'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, workspace);
});
it('empty sub_repos with no .git — returns startDir', () => {
writeConfig(workspace, { sub_repos: [] });
const child = path.join(workspace, 'child');
fs.mkdirSync(child, { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, child);
});
it('#1362: child has own .planning/ — returns child not workspace', () => {
writeConfig(workspace, { sub_repos: ['child'] });
const child = path.join(workspace, 'child');
fs.mkdirSync(path.join(child, '.planning'), { recursive: true });
const result = assertParity(child);
assert.strictEqual(result, child);
});
it('depth limit: .planning/ is 12 levels up — returns startDir (depth=10 cap)', () => {
// CANONICALIZATION NOTE: SDK has FIND_PROJECT_ROOT_MAX_DEPTH=10; CJS now
// also uses 10 (was unbounded before). At 12 levels the walk stops before
// reaching workspace, so startDir is returned.
fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true });
let dir = workspace;
for (let i = 1; i <= 12; i++) {
dir = path.join(dir, `l${i}`);
}
fs.mkdirSync(dir, { recursive: true });
const result = assertParity(dir);
assert.strictEqual(result, dir);
});
});

View File

@@ -1,195 +0,0 @@
'use strict';
/**
* Parity test — verifies that schema-detect.generated.cjs produces identical
* results to the compiled SDK ESM output for all exported functions.
*
* SDK side: import('../sdk/dist/query/schema-detect.js')
* CJS side: require('../get-shit-done/bin/lib/schema-detect.generated.cjs')
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { createRequire } = require('node:module');
const requireFromRoot = createRequire(__filename);
// CJS side — direct require works fine
const cjs = requireFromRoot('../get-shit-done/bin/lib/schema-detect.generated.cjs');
// ── detectSchemaFiles ─────────────────────────────────────────────────────
describe('schema-detect-generator parity: detectSchemaFiles', async () => {
const sdk = await import('../sdk/dist/query/schema-detect.js');
const fixtures = [
{
label: 'detects prisma schema',
files: ['prisma/schema.prisma'],
expectedDetected: true,
expectedOrms: ['prisma'],
},
{
label: 'detects drizzle schema',
files: ['drizzle/schema.ts'],
expectedDetected: true,
expectedOrms: ['drizzle'],
},
{
label: 'detects supabase migration',
files: ['supabase/migrations/001_init.sql'],
expectedDetected: true,
expectedOrms: ['supabase'],
},
{
label: 'detects payload collection',
files: ['src/collections/Users.ts'],
expectedDetected: true,
expectedOrms: ['payload'],
},
{
label: 'detects typeorm entity',
files: ['src/entities/User.ts'],
expectedDetected: true,
expectedOrms: ['typeorm'],
},
{
label: 'no schema files returns not detected',
files: ['src/components/Button.tsx', 'src/styles/main.css'],
expectedDetected: false,
expectedOrms: [],
},
{
label: 'multiple ORMs detected',
files: ['prisma/schema.prisma', 'drizzle/schema.ts'],
expectedDetected: true,
expectedOrms: ['prisma', 'drizzle'],
},
{
label: 'normalizes Windows backslash paths',
files: ['prisma\\schema.prisma'],
expectedDetected: true,
expectedOrms: ['prisma'],
},
{
label: 'empty file list returns not detected',
files: [],
expectedDetected: false,
expectedOrms: [],
},
];
for (const { label, files, expectedDetected, expectedOrms } of fixtures) {
test(label, () => {
const sdkResult = sdk.detectSchemaFiles(files);
const cjsResult = cjs.detectSchemaFiles(files);
assert.strictEqual(sdkResult.detected, expectedDetected, `SDK detected: ${label}`);
assert.deepStrictEqual(sdkResult.orms.sort(), expectedOrms.sort(), `SDK orms: ${label}`);
assert.strictEqual(cjsResult.detected, expectedDetected, `CJS detected: ${label}`);
assert.deepStrictEqual(cjsResult.orms.sort(), expectedOrms.sort(), `CJS orms: ${label}`);
// SDK and CJS must agree on detected and orms
assert.strictEqual(sdkResult.detected, cjsResult.detected, `SDK/CJS parity detected: ${label}`);
assert.deepStrictEqual(sdkResult.orms.sort(), cjsResult.orms.sort(), `SDK/CJS parity orms: ${label}`);
});
}
});
// ── checkSchemaDrift ──────────────────────────────────────────────────────
describe('schema-detect-generator parity: checkSchemaDrift', async () => {
const sdk = await import('../sdk/dist/query/schema-detect.js');
const fixtures = [
{
label: 'no schema files — no drift',
changedFiles: ['src/components/Button.tsx'],
executionLog: '',
options: {},
expectedDriftDetected: false,
expectedBlocking: false,
},
{
label: 'prisma changed with push evidence — no drift',
changedFiles: ['prisma/schema.prisma'],
executionLog: 'running: npx prisma db push --accept-data-loss',
options: {},
expectedDriftDetected: false,
expectedBlocking: false,
},
{
label: 'prisma changed without push — drift blocking',
changedFiles: ['prisma/schema.prisma'],
executionLog: 'tsc && vitest run',
options: {},
expectedDriftDetected: true,
expectedBlocking: true,
},
{
label: 'drift with skipCheck=true — not blocking',
changedFiles: ['prisma/schema.prisma'],
executionLog: 'tsc && vitest run',
options: { skipCheck: true },
expectedDriftDetected: true,
expectedBlocking: false,
},
];
for (const { label, changedFiles, executionLog, options, expectedDriftDetected, expectedBlocking } of fixtures) {
test(label, () => {
const sdkResult = sdk.checkSchemaDrift(changedFiles, executionLog, options);
const cjsResult = cjs.checkSchemaDrift(changedFiles, executionLog, options);
assert.strictEqual(sdkResult.driftDetected, expectedDriftDetected, `SDK driftDetected: ${label}`);
assert.strictEqual(sdkResult.blocking, expectedBlocking, `SDK blocking: ${label}`);
assert.strictEqual(cjsResult.driftDetected, expectedDriftDetected, `CJS driftDetected: ${label}`);
assert.strictEqual(cjsResult.blocking, expectedBlocking, `CJS blocking: ${label}`);
// Full structural parity between SDK and CJS
assert.deepStrictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── detectSchemaOrm (CJS-only compat export) ──────────────────────────────
describe('schema-detect-generator parity: detectSchemaOrm (CJS compat)', () => {
test('returns ORM info for known orm', () => {
const info = cjs.detectSchemaOrm('prisma');
assert.ok(info !== null, 'prisma orm info should not be null');
assert.ok(typeof info.pushCommand === 'string', 'pushCommand should be string');
assert.ok(Array.isArray(info.evidencePatterns), 'evidencePatterns should be array');
});
test('returns null for unknown orm', () => {
const info = cjs.detectSchemaOrm('unknown_orm');
assert.strictEqual(info, null, 'unknown orm should return null');
});
test('returns info for all 5 known ORMs', () => {
const orms = ['payload', 'prisma', 'drizzle', 'supabase', 'typeorm'];
for (const orm of orms) {
const info = cjs.detectSchemaOrm(orm);
assert.ok(info !== null, `${orm} info should not be null`);
}
});
});
// ── SCHEMA_PATTERNS and ORM_INFO exports (compat) ────────────────────────
describe('schema-detect-generator: SCHEMA_PATTERNS and ORM_INFO exported', () => {
test('SCHEMA_PATTERNS is an array', () => {
assert.ok(Array.isArray(cjs.SCHEMA_PATTERNS), 'SCHEMA_PATTERNS should be an array');
assert.ok(cjs.SCHEMA_PATTERNS.length > 0, 'SCHEMA_PATTERNS should not be empty');
});
test('ORM_INFO has known orm keys', () => {
const orms = ['payload', 'prisma', 'drizzle', 'supabase', 'typeorm'];
for (const orm of orms) {
assert.ok(orm in cjs.ORM_INFO, `ORM_INFO should have key: ${orm}`);
}
});
});

View File

@@ -1,136 +0,0 @@
'use strict';
/**
* Parity test — verifies that secrets.generated.cjs produces identical
* results to the compiled SDK ESM output for all exported functions.
*
* SDK side: import('../sdk/dist/query/secrets.js')
* CJS side: require('../get-shit-done/bin/lib/secrets.generated.cjs')
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { createRequire } = require('node:module');
const requireFromRoot = createRequire(__filename);
// CJS side — direct require works fine
const cjs = requireFromRoot('../get-shit-done/bin/lib/secrets.generated.cjs');
// ── SECRET_CONFIG_KEYS ────────────────────────────────────────────────────
describe('secrets-generator parity: SECRET_CONFIG_KEYS', async () => {
const sdk = await import('../sdk/dist/query/secrets.js');
test('contains same keys as SDK', () => {
const sdkKeys = [...sdk.SECRET_CONFIG_KEYS].sort();
const cjsKeys = [...cjs.SECRET_CONFIG_KEYS].sort();
assert.deepStrictEqual(cjsKeys, sdkKeys, 'SDK/CJS parity: SECRET_CONFIG_KEYS');
});
test('contains brave_search', () => {
assert.ok(cjs.SECRET_CONFIG_KEYS.has('brave_search'));
assert.ok(sdk.SECRET_CONFIG_KEYS.has('brave_search'));
});
test('contains firecrawl', () => {
assert.ok(cjs.SECRET_CONFIG_KEYS.has('firecrawl'));
assert.ok(sdk.SECRET_CONFIG_KEYS.has('firecrawl'));
});
test('contains exa_search', () => {
assert.ok(cjs.SECRET_CONFIG_KEYS.has('exa_search'));
assert.ok(sdk.SECRET_CONFIG_KEYS.has('exa_search'));
});
});
// ── isSecretKey ────────────────────────────────────────────────────────────
describe('secrets-generator parity: isSecretKey', async () => {
const sdk = await import('../sdk/dist/query/secrets.js');
const fixtures = [
{ label: 'brave_search is secret', key: 'brave_search', expected: true },
{ label: 'firecrawl is secret', key: 'firecrawl', expected: true },
{ label: 'exa_search is secret', key: 'exa_search', expected: true },
{ label: 'non-secret key returns false', key: 'model', expected: false },
{ label: 'empty string returns false', key: '', expected: false },
{ label: 'unrelated string returns false', key: 'api_key', expected: false },
];
for (const { label, key, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.isSecretKey(key);
const cjsResult = cjs.isSecretKey(key);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── maskSecret ────────────────────────────────────────────────────────────
describe('secrets-generator parity: maskSecret', async () => {
const sdk = await import('../sdk/dist/query/secrets.js');
const fixtures = [
{ label: 'null returns (unset)', value: null, expected: '(unset)' },
{ label: 'undefined returns (unset)', value: undefined, expected: '(unset)' },
{ label: 'empty string returns (unset)', value: '', expected: '(unset)' },
{ label: 'short string (< 8) returns ****', value: 'abc', expected: '****' },
{ label: '7-char string returns ****', value: '1234567', expected: '****' },
{ label: '8-char string returns ****<last-4>', value: '12345678', expected: '****5678' },
{ label: 'long string returns ****<last-4>', value: 'sk-ant-abc123def456', expected: '****f456' },
];
for (const { label, value, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.maskSecret(value);
const cjsResult = cjs.maskSecret(value);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// ── maskIfSecret ──────────────────────────────────────────────────────────
describe('secrets-generator parity: maskIfSecret', async () => {
const sdk = await import('../sdk/dist/query/secrets.js');
const fixtures = [
{
label: 'secret key gets masked',
key: 'brave_search',
value: 'sk-ant-12345678',
expectedType: 'string',
expectedValue: '****5678',
},
{
label: 'non-secret key returns value unchanged',
key: 'model',
value: 'claude-opus-4-5',
expectedType: 'string',
expectedValue: 'claude-opus-4-5',
},
{
label: 'secret key with null value returns (unset)',
key: 'firecrawl',
value: null,
expectedType: 'string',
expectedValue: '(unset)',
},
];
for (const { label, key, value, expectedValue } of fixtures) {
test(label, () => {
const sdkResult = sdk.maskIfSecret(key, value);
const cjsResult = cjs.maskIfSecret(key, value);
assert.strictEqual(sdkResult, expectedValue, `SDK: ${label}`);
assert.strictEqual(cjsResult, expectedValue, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});

View File

@@ -1,261 +0,0 @@
'use strict';
/**
* Parity test — verifies that state-document.generated.cjs produces identical
* results to the compiled SDK ESM output for all exported functions.
*
* SDK side: require('../sdk/dist/state/index.js') via createRequire
* CJS side: require('../get-shit-done/bin/lib/state-document.generated.cjs')
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { createRequire } = require('node:module');
// The SDK dist is ESM; wrap with createRequire targeting the project root so
// Node resolves the path correctly from this CJS context.
const requireFromRoot = createRequire(__filename);
// CJS side — direct require works fine
const cjs = requireFromRoot('../get-shit-done/bin/lib/state-document.generated.cjs');
describe('state-document-generator parity: stateReplaceFieldWithFallback', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
label: 'primary hit',
content: 'Status: old\nState: backup',
primary: 'Status',
fallback: 'State',
value: 'new',
expected: 'Status: new\nState: backup',
},
{
label: 'fallback hit',
content: 'Other: something\nState: backup',
primary: 'Status',
fallback: 'State',
value: 'new',
expected: 'Other: something\nState: new',
},
{
label: 'neither hit returns unchanged content',
content: 'Other: something\nAnother: value',
primary: 'Status',
fallback: 'State',
value: 'new',
expected: 'Other: something\nAnother: value',
},
];
for (const { label, content, primary, fallback, value, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.stateReplaceFieldWithFallback(content, primary, fallback, value);
const cjsResult = cjs.stateReplaceFieldWithFallback(content, primary, fallback, value);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
describe('state-document-generator parity: normalizeStateStatus', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{ label: 'paused via "paused"', status: 'paused', expected: 'paused' },
{ label: 'paused via "stopped"', status: 'stopped', expected: 'paused' },
{ label: 'paused via non-null pausedAt', status: 'active', pausedAt: '2024-01-01', expected: 'paused' },
{ label: 'executing via "executing"', status: 'executing', expected: 'executing' },
{ label: 'executing via "in progress"', status: 'in progress', expected: 'executing' },
{ label: 'executing via "ready to execute"', status: 'ready to execute', expected: 'executing' },
{ label: 'planning via "planning"', status: 'planning', expected: 'planning' },
{ label: 'discussing via "discussing"', status: 'discussing', expected: 'discussing' },
{ label: 'verifying via "verif"', status: 'verifying', expected: 'verifying' },
{ label: 'completed via "complete"', status: 'completed', expected: 'completed' },
{ label: 'completed via "done"', status: 'done', expected: 'completed' },
{ label: 'unknown fallback', status: 'something-else', expected: 'something-else' },
{ label: 'null status', status: null, expected: 'unknown' },
];
for (const { label, status, pausedAt, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.normalizeStateStatus(status, pausedAt);
const cjsResult = cjs.normalizeStateStatus(status, pausedAt);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
describe('state-document-generator parity: computeProgressPercent', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{ label: 'only plans data', cp: 3, tp: 10, cf: null, tf: null, expected: 30 },
{ label: 'only phases data', cp: null, tp: null, cf: 2, tf: 4, expected: 50 },
{ label: 'both present uses min', cp: 8, tp: 10, cf: 3, tf: 10, expected: 30 },
{ label: 'neither returns null', cp: null, tp: null, cf: null, tf: null, expected: null },
{ label: 'total of 0 treated as no data', cp: 0, tp: 0, cf: null, tf: null, expected: null },
];
for (const { label, cp, tp, cf, tf, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.computeProgressPercent(cp, tp, cf, tf);
const cjsResult = cjs.computeProgressPercent(cp, tp, cf, tf);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
describe('state-document-generator parity: shouldPreserveExistingProgress', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
label: 'existing exceeds derived on total_phases → true',
existing: { total_phases: 10 },
derived: { total_phases: 5 },
expected: true,
},
{
label: 'derived exceeds existing → false',
existing: { total_phases: 5 },
derived: { total_phases: 10 },
expected: false,
},
{
label: 'malformed input (non-object) → false',
existing: null,
derived: { total_phases: 5 },
expected: false,
},
{
label: 'both null → false',
existing: null,
derived: null,
expected: false,
},
];
for (const { label, existing, derived, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.shouldPreserveExistingProgress(existing, derived);
const cjsResult = cjs.shouldPreserveExistingProgress(existing, derived);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
describe('state-document-generator parity: normalizeProgressNumbers', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
label: 'coerces all five tracked keys to numbers',
input: { total_phases: '10', completed_phases: '3', total_plans: '5', completed_plans: '2', percent: '60' },
expected: { total_phases: 10, completed_phases: 3, total_plans: 5, completed_plans: 2, percent: 60 },
},
{
label: 'non-object null returned unchanged',
input: null,
expected: null,
},
{
label: 'extra keys preserved untouched',
input: { total_phases: '4', extra_key: 'hello' },
expected: { total_phases: 4, extra_key: 'hello' },
},
];
for (const { label, input, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.normalizeProgressNumbers(input);
const cjsResult = cjs.normalizeProgressNumbers(input);
assert.deepStrictEqual(sdkResult, expected, `SDK: ${label}`);
assert.deepStrictEqual(cjsResult, expected, `CJS: ${label}`);
assert.deepStrictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
// SDK ESM side — dynamically import so we can test both; wrap in a top-level
// async test suite.
describe('state-document-generator parity: stateExtractField', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
label: 'bold pattern',
content: 'Some content\n**FieldName:** the value\nMore content',
fieldName: 'FieldName',
expected: 'the value',
},
{
label: 'plain pattern',
content: 'Some content\nFieldName: the value\nMore content',
fieldName: 'FieldName',
expected: 'the value',
},
{
label: 'missing field returns null',
content: 'Some content\nOtherField: something\nMore content',
fieldName: 'FieldName',
expected: null,
},
];
for (const { label, content, fieldName, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.stateExtractField(content, fieldName);
const cjsResult = cjs.stateExtractField(content, fieldName);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});
describe('state-document-generator parity: stateReplaceField', async () => {
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
label: 'bold replace',
content: 'Some content\n**Status:** old value\nMore content',
fieldName: 'Status',
newValue: 'new value',
expected: 'Some content\n**Status:** new value\nMore content',
},
{
label: 'plain replace',
content: 'Some content\nStatus: old value\nMore content',
fieldName: 'Status',
newValue: 'new value',
expected: 'Some content\nStatus: new value\nMore content',
},
{
label: 'missing field returns null',
content: 'Some content\nOtherField: something\nMore content',
fieldName: 'Status',
newValue: 'new value',
expected: null,
},
];
for (const { label, content, fieldName, newValue, expected } of fixtures) {
test(label, () => {
const sdkResult = sdk.stateReplaceField(content, fieldName, newValue);
const cjsResult = cjs.stateReplaceField(content, fieldName, newValue);
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
});
}
});

View File

@@ -1,159 +0,0 @@
'use strict';
/**
* CJS parity test — Workstream Inventory Builder generator.
*
* For every fixture, asserts that the compiled SDK ESM module and the
* generated CJS artifact produce byte-identical output.
*/
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
// ─── Shared fixtures ──────────────────────────────────────────────────────────
function minimalInputs(overrides = {}) {
return {
name: 'my-ws',
projectDir: '/project',
workstreamDir: '/project/.planning/workstreams/my-ws',
phaseDirNames: [],
activeWorkstreamName: null,
phaseFilesCounts: [],
roadmapPhaseCount: 0,
stateProjection: { status: 'unknown', current_phase: null, last_activity: null },
filesExist: { roadmap: false, state: false, requirements: false },
...overrides,
};
}
const FIXTURES = [
{
label: 'empty inventory: no phase dirs, no STATE.md',
inputs: minimalInputs(),
},
{
label: 'one phase in_progress (partial plan completion)',
inputs: minimalInputs({
phaseDirNames: ['01-alpha'],
phaseFilesCounts: [{ directory: '01-alpha', planCount: 3, summaryCount: 1 }],
roadmapPhaseCount: 1,
stateProjection: { status: 'executing', current_phase: '01-alpha', last_activity: '2026-05-01' },
filesExist: { roadmap: true, state: true, requirements: false },
}),
},
{
label: 'one phase complete (summary_count >= plan_count)',
inputs: minimalInputs({
phaseDirNames: ['01-alpha'],
phaseFilesCounts: [{ directory: '01-alpha', planCount: 2, summaryCount: 2 }],
roadmapPhaseCount: 1,
stateProjection: { status: 'milestone complete', current_phase: null, last_activity: '2026-04-01' },
filesExist: { roadmap: true, state: true, requirements: true },
}),
},
{
label: 'one phase pending (plan_count is 0)',
inputs: minimalInputs({
phaseDirNames: ['01-alpha'],
phaseFilesCounts: [{ directory: '01-alpha', planCount: 0, summaryCount: 0 }],
roadmapPhaseCount: 1,
stateProjection: { status: 'planning', current_phase: null, last_activity: null },
}),
},
{
label: 'multiple phases with mixed statuses',
inputs: minimalInputs({
phaseDirNames: ['01-alpha', '02-beta', '03-gamma'],
phaseFilesCounts: [
{ directory: '01-alpha', planCount: 2, summaryCount: 2 },
{ directory: '02-beta', planCount: 3, summaryCount: 1 },
{ directory: '03-gamma', planCount: 0, summaryCount: 0 },
],
roadmapPhaseCount: 3,
stateProjection: { status: 'executing', current_phase: '02-beta', last_activity: '2026-05-10' },
filesExist: { roadmap: true, state: true, requirements: false },
}),
},
{
label: 'progress_percent clamps to 100 when completedPhases > roadmapPhaseCount',
inputs: minimalInputs({
phaseDirNames: ['01-alpha', '02-beta', '03-gamma'],
phaseFilesCounts: [
{ directory: '01-alpha', planCount: 1, summaryCount: 1 },
{ directory: '02-beta', planCount: 1, summaryCount: 1 },
{ directory: '03-gamma', planCount: 1, summaryCount: 1 },
],
roadmapPhaseCount: 1,
stateProjection: { status: 'milestone complete', current_phase: null, last_activity: null },
filesExist: { roadmap: true, state: true, requirements: false },
}),
},
{
label: 'active workstream marker: active: true when activeWorkstreamName === name',
inputs: minimalInputs({
name: 'my-ws',
activeWorkstreamName: 'my-ws',
}),
},
{
label: 'active: false when activeWorkstreamName is a different workstream',
inputs: minimalInputs({
name: 'my-ws',
activeWorkstreamName: 'other-ws',
}),
},
];
const IS_COMPLETED_FIXTURES = [
{ status: 'milestone complete', expected: true },
{ status: 'Milestone Complete', expected: true },
{ status: 'archived', expected: true },
{ status: 'Archived', expected: true },
{ status: 'executing', expected: false },
{ status: 'planning', expected: false },
{ status: 'unknown', expected: false },
{ status: '', expected: false },
];
// ─── Test suite ───────────────────────────────────────────────────────────────
describe('workstream-inventory-builder generator parity (ESM dist vs generated CJS)', () => {
let sdkBuild, cjsModule;
before(async () => {
// Dynamic import of the ESM SDK dist (use pathToFileURL since we're in CJS context)
const path = require('path');
const { pathToFileURL } = require('url');
const distPath = path.resolve(__dirname, '..', 'sdk', 'dist', 'workstream', 'builder.js');
sdkBuild = await import(pathToFileURL(distPath).href);
// CJS require of the generated artifact
cjsModule = require('../get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs');
});
describe('buildWorkstreamInventory', () => {
for (const fixture of FIXTURES) {
test(fixture.label, () => {
const sdkResult = sdkBuild.buildWorkstreamInventory(fixture.inputs);
const cjsResult = cjsModule.buildWorkstreamInventory(fixture.inputs);
assert.deepStrictEqual(
cjsResult,
sdkResult,
`Parity failure for fixture "${fixture.label}"`,
);
});
}
});
describe('isCompletedInventory', () => {
for (const { status, expected } of IS_COMPLETED_FIXTURES) {
test(`isCompletedInventory("${status}") === ${expected}`, () => {
const sdkResult = sdkBuild.isCompletedInventory(status);
const cjsResult = cjsModule.isCompletedInventory(status);
assert.strictEqual(sdkResult, expected, `SDK result mismatch for "${status}"`);
assert.strictEqual(cjsResult, expected, `CJS result mismatch for "${status}"`);
assert.strictEqual(sdkResult, cjsResult, `Parity failure for "${status}"`);
});
}
});
});

View File

@@ -1,145 +0,0 @@
'use strict';
/**
* Parity test: workstream-name-policy.generated.cjs vs sdk/src/workstream-name-policy.ts
*
* Verifies that the generated CJS artifact matches the SDK source-of-truth
* for all exports: toWorkstreamSlug, hasInvalidPathSegment, isValidActiveWorkstreamName,
* validateWorkstreamName.
*
* Covers: Phase 6 (#3575) MIGRATE_ME resolution for workstream-name-policy.cjs.
*/
const assert = require('assert');
const { describe, test } = require('node:test');
const {
toWorkstreamSlug,
hasInvalidPathSegment,
isValidActiveWorkstreamName,
validateWorkstreamName,
} = require('../get-shit-done/bin/lib/workstream-name-policy.cjs');
// ─── toWorkstreamSlug ────────────────────────────────────────────────────────
describe('workstream-name-policy — toWorkstreamSlug', () => {
test('lowercases and collapses non-alphanumeric to hyphens', () => {
assert.strictEqual(toWorkstreamSlug('My Feature Branch'), 'my-feature-branch');
assert.strictEqual(toWorkstreamSlug('hello_world'), 'hello-world');
assert.strictEqual(toWorkstreamSlug('API v2'), 'api-v2');
});
test('strips leading/trailing hyphens', () => {
assert.strictEqual(toWorkstreamSlug('--foo--'), 'foo');
assert.strictEqual(toWorkstreamSlug(' spaces '), 'spaces');
});
test('handles empty and nullish values', () => {
assert.strictEqual(toWorkstreamSlug(''), '');
assert.strictEqual(toWorkstreamSlug(null), '');
assert.strictEqual(toWorkstreamSlug(undefined), '');
});
test('handles already-valid slug', () => {
assert.strictEqual(toWorkstreamSlug('my-feature'), 'my-feature');
assert.strictEqual(toWorkstreamSlug('v2'), 'v2');
});
});
// ─── hasInvalidPathSegment ───────────────────────────────────────────────────
describe('workstream-name-policy — hasInvalidPathSegment', () => {
test('returns true for names with forward slash', () => {
assert.strictEqual(hasInvalidPathSegment('foo/bar'), true);
});
test('returns true for names with backslash', () => {
assert.strictEqual(hasInvalidPathSegment('foo\\bar'), true);
});
test('returns true for bare dot', () => {
assert.strictEqual(hasInvalidPathSegment('.'), true);
});
test('returns true for double dot', () => {
assert.strictEqual(hasInvalidPathSegment('..'), true);
});
test('returns true for names containing dot-dot sequence', () => {
assert.strictEqual(hasInvalidPathSegment('foo..bar'), true);
assert.strictEqual(hasInvalidPathSegment('../etc'), true);
});
test('returns false for valid workstream names', () => {
assert.strictEqual(hasInvalidPathSegment('my-feature'), false);
assert.strictEqual(hasInvalidPathSegment('v2'), false);
assert.strictEqual(hasInvalidPathSegment('feature.experimental'), false);
assert.strictEqual(hasInvalidPathSegment('alpha_1'), false);
});
test('handles empty and nullish values', () => {
assert.strictEqual(hasInvalidPathSegment(''), false);
assert.strictEqual(hasInvalidPathSegment(null), false);
assert.strictEqual(hasInvalidPathSegment(undefined), false);
});
});
// ─── isValidActiveWorkstreamName ─────────────────────────────────────────────
describe('workstream-name-policy — isValidActiveWorkstreamName', () => {
test('returns true for valid alphanumeric names', () => {
assert.strictEqual(isValidActiveWorkstreamName('feature'), true);
assert.strictEqual(isValidActiveWorkstreamName('v2'), true);
assert.strictEqual(isValidActiveWorkstreamName('my-branch'), true);
assert.strictEqual(isValidActiveWorkstreamName('feature.experimental'), true);
assert.strictEqual(isValidActiveWorkstreamName('alpha_1'), true);
assert.strictEqual(isValidActiveWorkstreamName('A1'), true);
});
test('returns false for names starting with non-alphanumeric', () => {
assert.strictEqual(isValidActiveWorkstreamName('-feature'), false);
assert.strictEqual(isValidActiveWorkstreamName('.feature'), false);
assert.strictEqual(isValidActiveWorkstreamName('_feature'), false);
});
test('returns false for names with path traversal', () => {
assert.strictEqual(isValidActiveWorkstreamName('..'), false);
assert.strictEqual(isValidActiveWorkstreamName('../etc'), false);
assert.strictEqual(isValidActiveWorkstreamName('foo..bar'), false);
});
test('returns false for names with slashes', () => {
assert.strictEqual(isValidActiveWorkstreamName('foo/bar'), false);
assert.strictEqual(isValidActiveWorkstreamName('foo\\bar'), false);
});
test('returns false for names with spaces', () => {
assert.strictEqual(isValidActiveWorkstreamName('my feature'), false);
});
test('returns false for empty string', () => {
assert.strictEqual(isValidActiveWorkstreamName(''), false);
});
test('returns false for nullish values', () => {
assert.strictEqual(isValidActiveWorkstreamName(null), false);
assert.strictEqual(isValidActiveWorkstreamName(undefined), false);
});
});
// ─── validateWorkstreamName (SDK alias) ──────────────────────────────────────
describe('workstream-name-policy — validateWorkstreamName (SDK alias)', () => {
test('is an alias for isValidActiveWorkstreamName', () => {
const testCases = [
'feature', 'v2', 'my-branch', '-bad', '', null, undefined,
'foo/bar', '..', 'foo..bar', 'A1', 'alpha_1',
];
for (const tc of testCases) {
assert.strictEqual(
validateWorkstreamName(tc),
isValidActiveWorkstreamName(tc),
`validateWorkstreamName and isValidActiveWorkstreamName should agree on: ${JSON.stringify(tc)}`,
);
}
});
});