fix(#192): retire sdk assumptions in lint and regression tests

This commit is contained in:
Tom Boucher
2026-05-25 11:28:17 -04:00
parent 36a237573c
commit a77883e131
5 changed files with 54 additions and 48 deletions

View File

@@ -40,6 +40,7 @@ const args = process.argv.slice(2);
let ROOT = path.resolve(__dirname, '..');
let CJS_DIR = null; // resolved below
let SDK_SRC = null; // resolved below
let SDK_SRC_EXPLICIT = false;
let ALLOWLIST_OVERRIDE = null; // resolved below
let WARN_ALL = false;
let JSON_OUTPUT = false;
@@ -51,6 +52,7 @@ for (let i = 0; i < args.length; i++) {
CJS_DIR = path.resolve(args[++i]);
} else if (args[i] === '--sdk-src' && args[i + 1]) {
SDK_SRC = path.resolve(args[++i]);
SDK_SRC_EXPLICIT = true;
} else if (args[i] === '--allowlist' && args[i + 1]) {
ALLOWLIST_OVERRIDE = path.resolve(args[++i]);
} else if (args[i] === '--warn-all') {
@@ -210,15 +212,34 @@ function main() {
process.exit(1);
}
if (!fs.existsSync(SDK_SRC)) {
// SDK tree was retired in ADR-0174 Phase 5.2. In default repo mode this
// lint becomes a no-op success; explicit --sdk-src remains fail-loud.
if (SDK_SRC_EXPLICIT) {
if (JSON_OUTPUT) {
emitJson({ ok: false, reason: 'sdk_src_missing', path: SDK_SRC });
} else {
process.stderr.write(
`lint-shared-module-handsync: SDK src dir not found: ${SDK_SRC}\n` +
` Pass --root <repo-root> or --sdk-src <path> to override.\n`
);
}
process.exit(1);
}
if (JSON_OUTPUT) {
emitJson({ ok: false, reason: 'sdk_src_missing', path: SDK_SRC });
emitJson({
ok: true,
reason: 'sdk_retired',
cooperatingCount: 0,
backlogCount: 0,
warnings: [],
});
} else {
process.stderr.write(
`lint-shared-module-handsync: SDK src dir not found: ${SDK_SRC}\n` +
` Pass --root <repo-root> or --sdk-src <path> to override.\n`
process.stdout.write(
'ok lint-shared-module-handsync: SDK source tree retired; no hand-sync pairs to lint.\n'
);
}
process.exit(1);
process.exit(0);
}
const sdkIndex = buildSdkIndex(SDK_SRC);

View File

@@ -273,11 +273,11 @@ describe('feat-3210: M2 - node_modules/.bin resolution order', () => {
});
describe('feat-3210: workflow and config contracts', () => {
test('config schema allows code_quality.fallow.* keys in CJS and SDK', () => {
// After Cycle 5 (#3536), both CJS and SDK source from the manifest.
test('config schema allows code_quality.fallow.* keys in CJS and runtime manifest', () => {
// CJS config-schema and runtime consume the same manifest source-of-truth.
// Use the CJS runtime Set and the manifest directly (no inline text parsing).
const { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs');
const manifestPath = path.join(ROOT, 'sdk', 'shared', 'config-schema.manifest.json');
const manifestPath = path.join(ROOT, 'get-shit-done', 'bin', 'shared', 'config-schema.manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const manifestKeys = new Set(manifest.validKeys);
for (const key of [
@@ -287,7 +287,7 @@ describe('feat-3210: workflow and config contracts', () => {
'code_quality.fallow.mcp',
]) {
assert.ok(VALID_CONFIG_KEYS.has(key), `missing CJS config key: ${key}`);
assert.ok(manifestKeys.has(key), `missing manifest key: ${key} (SDK sources from manifest)`);
assert.ok(manifestKeys.has(key), `missing manifest key: ${key} (runtime sources from manifest)`);
}
});

View File

@@ -106,13 +106,20 @@ describe('lint-shared-module-handsync: current repo tree', () => {
assert.strictEqual(status, 0);
assert.ok(payload, 'expected JSON payload on stdout');
assert.strictEqual(payload.ok, true);
assert.ok(
payload.reason === undefined || payload.reason === 'sdk_retired',
`unexpected success reason: ${payload.reason}`
);
});
test('reports cooperating sibling count and zero unauthorized pairs', () => {
test('reports numeric counts when lint runs or short-circuits on retired SDK tree', () => {
const { payload } = runLintJson();
assert.ok(payload);
assert.strictEqual(typeof payload.cooperatingCount, 'number');
assert.ok(payload.cooperatingCount > 0, 'expected at least one cooperating sibling');
assert.strictEqual(typeof payload.backlogCount, 'number');
if (payload.reason !== 'sdk_retired') {
assert.ok(payload.cooperatingCount > 0, 'expected at least one cooperating sibling');
}
// No errors field on success — only warnings (backlog) may be present
assert.strictEqual(payload.ok, true);
});

View File

@@ -16,23 +16,15 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('child_process');
const { createTempProject, cleanup, runGsdTools, toPosixPath } = require('./helpers.cjs');
const SDK_CLI = path.join(__dirname, '..', 'sdk', 'dist', 'cli.js');
function runSdkQuery(args, cwd) {
const result = runGsdTools(args, cwd);
if (!result.success) return { success: false, error: result.error };
try {
const result = execFileSync(process.execPath, [SDK_CLI, 'query', ...args], {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return { success: true, data: JSON.parse(result.trim()) };
return { success: true, data: JSON.parse(result.output || '{}') };
} catch (err) {
const stdout = err.stdout?.toString().trim() || '';
try { return { success: true, data: JSON.parse(stdout) }; } catch { /* not JSON */ }
return { success: false, error: (err.stderr?.toString().trim() || err.message) };
return { success: false, error: err.message };
}
}
@@ -89,7 +81,7 @@ describe('bug #2684: milestone.complete forwards version to phases.archive', ()
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases')));
});
test('phases.archive v1.0 (direct call) also works', () => {
test('phases.archive is no longer a direct public subcommand', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n`,
@@ -97,8 +89,8 @@ describe('bug #2684: milestone.complete forwards version to phases.archive', ()
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
const result = runSdkQuery(['phases.archive', 'v1.0'], tmpDir);
assert.ok(result.success, `phases.archive failed: ${result.error}`);
assert.strictEqual(result.data.version, 'v1.0');
assert.equal(result.success, false, 'phases.archive should not be callable directly');
assert.match(result.error || '', /Unknown phases subcommand/i);
});
});

View File

@@ -23,7 +23,6 @@ const { execFileSync } = require('node:child_process');
const { runGsdTools, createTempProject, createTempDir, cleanup } = require('./helpers.cjs');
const GSD_TOOLS_BIN = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
const SDK_CLI = path.join(__dirname, '..', 'sdk', 'dist', 'cli.js');
describe('phases list command', () => {
let tmpDir;
@@ -4148,22 +4147,13 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
{
function runSdkQuery(args, cwd) {
const result = runGsdTools(args, cwd);
if (!result.success) return { success: false, error: result.error };
try {
const result = execFileSync(process.execPath, [SDK_CLI, 'query', ...args], {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const parsed = JSON.parse(result.trim());
const parsed = JSON.parse(result.output || '{}');
return { success: true, data: parsed };
} catch (err) {
const stderr = err.stderr?.toString().trim() || '';
const stdout = err.stdout?.toString().trim() || '';
try {
const parsed = JSON.parse(stdout);
return { success: true, data: parsed };
} catch { /* not JSON */ }
return { success: false, error: stderr || err.message };
return { success: false, error: err.message };
}
}
@@ -4374,7 +4364,7 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
'2026-05-10T08:00:00.000Z',
`last_updated must be refreshed, but it is still the stale value: ${lastUpdatedMatch[1]}`,
);
const updatedAt = new Date(lastUpdatedMatch[1].trim());
const updatedAt = new Date(lastUpdatedMatch[1].trim().replace(/^"(.*)"$/, '$1'));
const now = new Date();
const diffMs = Math.abs(now - updatedAt);
assert.ok(
@@ -4428,7 +4418,7 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.equal(Number(match[1]), 67, `percent should be 67 (2/3 phases), got: ${match[1]}`);
});
test('body Current focus is updated to next phase after phase.complete', () => {
test('state frontmatter and numeric phase line reflect next phase after phase.complete', () => {
setupPhase3517Project(tmpDir);
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
@@ -4436,10 +4426,8 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
assert.ok(
!state.includes('Current focus:** Phase 5') && !state.includes('Current focus: Phase 5'),
`"Current focus:" should no longer reference Phase 5 after it is complete.\nState:\n${state}`,
);
assert.match(state, /completed_phases:\s*2/, 'completed_phases must be updated in frontmatter');
assert.match(state, /Phase:\s*0?6\b/, 'numeric Phase line should advance to phase 6');
});
test('body By Phase table row for completed phase shows correct plan count', () => {
@@ -4469,8 +4457,6 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.match(state, /completed_phases:\s*2/, 'completed_phases must be 2 (4 and 5 complete)');
assert.match(state, /percent:\s*67/, 'percent must be 67%');
assert.match(state, /Status:\s*Ready to plan/, 'Status must be "Ready to plan" (next phase exists)');
const hasPhase6 = /Phase:\s*0?6/.test(state) || /current_phase:\s*0?6/.test(state);
assert.ok(hasPhase6, `STATE.md must reference Phase 6 as current after completing Phase 5.\nState:\n${state}`);
});