Phase 2 of the CJS↔SDK hard-seam migration (parent #3524). Eliminates the structural drift surface that produced bug class After this phase, neither bin/lib/ nor sdk/src/ defines CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the four legacy-key normalizations inline. All come from one canonical source: the Configuration Module (sdk/src/configuration/index.ts) + two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json). The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs) with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs). - sdk/shared/config-defaults.manifest.json — canonical nested defaults, union of CJS + SDK keys (includes security_*, post_planning_gaps, agent_skills, mode, every git/workflow/hooks sub-section). - sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array, RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source strings (regex reconstructed at runtime). - sdk/src/configuration/index.ts — source of truth. Exports loadConfig (pure read), normalizeLegacyKeys (pure, idempotent, returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk (explicit opt-in disk writeback), plus CONFIG_DEFAULTS, VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS. - sdk/src/configuration/index.test.ts — 29 vitest pinning tests. - sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString() inspection of compiled SDK dist, plus brace-balanced text scan for internal helpers, matching the Phase 1 pattern). - sdk/scripts/check-configuration-fresh.mjs — CI freshness gate. - tests/configuration-generator.test.cjs — 27 parity assertions (CJS-generated == SDK source). - tests/configuration-migrate-config.test.cjs — 3 cases for the new gsd-tools migrate-config subcommand. - bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat projection at the load boundary to preserve the existing flat-shape return contract for the ~21 CJS test files and 100+ consumers. All four legacy-key migration blocks (branching_strategy, sub_repos, multiRepo, depth — historically lines 351-358, 388-397, 401-408, 416-423) collapse to a single normalizeLegacyKeys call in each code path. The inline platformWriteSync writeback stays for now to preserve sync loadConfig semantics; the new async migrateOnDisk is reachable via gsd-tools migrate-config. - bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the generated Module. - bin/lib/config.cjs: adds cmdMigrateConfig handler (calls migrateOnDisk on the explicit user-driven path). - bin/gsd-tools.cjs: wires migrate-config into command dispatch. - sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults from the Module. loadConfig now calls normalizeLegacyKeys before mergeDefaults (replaces the inline branching_strategy graft). - sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from the Module. - tests/config-schema-sdk-parity.test.cjs: refactored from "CJS Set equals SDK Set" (trivially true post-migration) to "both sides source from the manifest" — structural plus runtime invariant. - Four other tests that text-grepped source files for valid keys (plan-review-convergence, bug-3212, bug-2492, feat-3210) are updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON lookups. - CONTEXT.md: new Configuration Module entry with full Interface contract. - Root package.json: check:configuration-fresh proxy script. - sdk/package.json: gen:configuration + check:configuration-fresh. - .githooks/pre-commit: configuration drift block. - .github/workflows/test.yml: configuration drift step after the alias drift check. - 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests across migrate-config + parity refactor) - 1872 SDK vitest tests pass - 29 Configuration Module vitest fixtures - 27 CJS/SDK parity fixtures - Net diff: +388 / −519 = 131-line reduction across the seven cycles, despite adding the new Module, manifests, generator, freshness check, and two new test files. 1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys (resolve_model_ids: false, context_window: 200000, phase_naming, claude_md_path, git.create_tag, workflow.security_*, workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*). Consumers accessing via [key: string]: unknown index get the manifest default instead of undefined. 2. SDK mergeDefaults is now proper recursive deep-merge instead of spread-per-section. Overlay { workflow: { research: false } } now preserves sibling workflow keys; previously it replaced the entire workflow section with only research + the section's defaults. Semantically identical for the common case; strictly better for partial nested overrides. 3. New gsd-tools migrate-config CLI subcommand for the explicit, opt-in on-disk migration path. Closes #3536.
103 lines
5.3 KiB
JavaScript
103 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
// allow-test-rule: source-text-is-product [#3212]
|
|
// The bug is in workflow/config contracts consumed by agents at runtime.
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { spawnSync } = require('node:child_process');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(ROOT, relativePath), 'utf8');
|
|
}
|
|
|
|
function runGsd(args, cwd) {
|
|
return spawnSync(process.execPath, [path.join(ROOT, 'get-shit-done/bin/gsd-tools.cjs'), ...args], {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
describe('bug #3212 execute-phase stall detection and safe resume', () => {
|
|
test('config schemas register executor stall detector keys', () => {
|
|
// After Cycle 5 (#3536), both CJS and SDK source from the manifest.
|
|
// Use the CJS runtime Set for CJS; use the manifest directly for SDK-side
|
|
// verification (since config-schema.ts no longer has inline literals).
|
|
const { VALID_CONFIG_KEYS: cjsKeys } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
|
const manifest = JSON.parse(read('sdk/shared/config-schema.manifest.json'));
|
|
const manifestKeys = new Set(manifest.validKeys);
|
|
|
|
for (const key of ['executor.stall_detect_interval_minutes', 'executor.stall_threshold_minutes']) {
|
|
assert.ok(cjsKeys.has(key), `CJS VALID_CONFIG_KEYS must include ${key}`);
|
|
assert.ok(manifestKeys.has(key), `Manifest validKeys must include ${key} (SDK sources from manifest)`);
|
|
}
|
|
});
|
|
|
|
test('configuration docs describe stall detector defaults', () => {
|
|
const docs = read('docs/CONFIGURATION.md');
|
|
|
|
assert.match(docs, /`executor\.stall_detect_interval_minutes`\s*\|\s*number\s*\|\s*`5`/);
|
|
assert.match(docs, /`executor\.stall_threshold_minutes`\s*\|\s*number\s*\|\s*`10`/);
|
|
});
|
|
|
|
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 }));
|
|
fs.mkdirSync(path.join(tmp, '.planning'));
|
|
fs.writeFileSync(path.join(tmp, '.planning/config.json'), '{}\n');
|
|
|
|
const interval = runGsd(['config-get', 'executor.stall_detect_interval_minutes', '--raw'], tmp);
|
|
const threshold = runGsd(['config-get', 'executor.stall_threshold_minutes', '--raw'], tmp);
|
|
|
|
assert.equal(interval.status, 0, interval.stderr);
|
|
assert.equal(interval.stdout.trim(), '5');
|
|
assert.equal(threshold.status, 0, threshold.stderr);
|
|
assert.equal(threshold.stdout.trim(), '10');
|
|
});
|
|
|
|
test('execute-phase verifies partial-plan drift before dispatch', () => {
|
|
const workflow = read('get-shit-done/workflows/execute-phase.md');
|
|
|
|
assert.match(workflow, /<step name="safe_resume_gate"/, 'execute-phase must define a safe_resume_gate step');
|
|
assert.match(workflow, /git log --oneline --grep="\$\{CURRENT_PLAN_ID\}"/, 'safe resume gate must check commits for the current plan id');
|
|
assert.match(workflow, /SUMMARY.md is missing/, 'safe resume gate must detect production commits with missing SUMMARY.md');
|
|
assert.match(workflow, /close out manually/, 'safe resume gate must offer manual close-out recovery');
|
|
assert.match(workflow, /re-execute from scratch/, 'safe resume gate must offer re-execute recovery');
|
|
assert.match(workflow, /mark-and-skip/, 'safe resume gate must offer mark-and-skip recovery');
|
|
});
|
|
|
|
test('execute-phase has configurable executor stall surveillance after dispatch', () => {
|
|
const workflow = read('get-shit-done/workflows/execute-phase.md');
|
|
|
|
assert.match(workflow, /EXECUTOR_STALL_INTERVAL_MINUTES=.*executor\.stall_detect_interval_minutes/);
|
|
assert.match(workflow, /EXECUTOR_STALL_THRESHOLD_MINUTES=.*executor\.stall_threshold_minutes/);
|
|
assert.match(workflow, /DISPATCH_TS=/, 'execute-phase must record dispatch timestamp');
|
|
assert.match(workflow, /EXPECTED_BRANCH=/, 'execute-phase must record expected branch');
|
|
assert.match(workflow, /git log "\$\{EXPECTED_BRANCH\}" --since="\$\{DISPATCH_TS\}"/, 'stall check must inspect branch commits since dispatch');
|
|
assert.match(workflow, /continue waiting/, 'stall warning must offer continue waiting');
|
|
assert.match(workflow, /kill and retry/, 'stall warning must offer kill and retry');
|
|
assert.match(workflow, /kill and switch to inline execution/, 'stall warning must offer inline fallback');
|
|
});
|
|
|
|
test('execute-plan documents atomic close-out invariant', () => {
|
|
const workflow = read('get-shit-done/workflows/execute-plan.md');
|
|
|
|
assert.match(workflow, /<atomic_close_out_invariant>/, 'execute-plan must contain a formal atomic close-out invariant');
|
|
assert.match(workflow, /production-code commit\(s\) -> SUMMARY commit -> STATE\/ROADMAP update/, 'invariant must name the legal close-out sequence');
|
|
assert.match(workflow, /only legal half-state is mid-production-commits/, 'invariant must define the only legal half-state');
|
|
});
|
|
|
|
test('forensics includes the partial-plan drift detector', () => {
|
|
const workflow = read('get-shit-done/workflows/forensics.md');
|
|
|
|
assert.match(workflow, /Partial-plan Drift Detection/);
|
|
assert.match(workflow, /commits exist but SUMMARY.md is missing/);
|
|
assert.match(workflow, /safe-resume verifier/);
|
|
});
|
|
});
|