* feat(sdk): golden parity harness and query handler CJS alignment (#2302 Track A) Golden/read-only parity tests and registry alignment, query handler fixes (check-completion, state-mutation, commit, validate, summary, etc.), and WAITING.json dual-write for .gsd/.planning readers. Refs gsd-build/get-shit-done#2341 * fix(sdk): getMilestoneInfo matches GSD ROADMAP (🟡, last bold, STATE fallback) - Recognize in-flight 🟡 milestone bullets like 🚧. - Derive from last **vX.Y Title** before ## Phases when emoji absent. - Fall back to STATE.md milestone when ROADMAP is missing; use last bare vX.Y in cleaned text instead of first (avoids v1.0 from shipped list). - Fixes init.execute-phase milestone_version and buildStateFrontmatter after state.begin-phase (syncStateFrontmatter). * feat(sdk): phase list, plan task structure, requirements extract handlers - Register phase.list-plans, phase.list-artifacts, plan.task-structure, requirements.extract-from-plans (SDK-only; golden-policy exceptions). - Add unit tests; document in QUERY-HANDLERS.md. - writeProfile: honor --output, render dimensions, return profile_path and dimensions_scored. * feat(sdk): centralize getGsdAgentsDir in query helpers Extract agent directory resolution to helpers (GSD_AGENTS_DIR, primary ~/.claude/agents, legacy path). Use from init and docs-init init bundles. docs(15): add 15-CONTEXT for autonomous phase-15 run. * feat(sdk): query CLI CJS fallback and session correlation - createRegistry(eventStream, sessionId) threads correlation into mutation events - gsd-sdk query falls back to gsd-tools.cjs when no native handler matches (disable with GSD_QUERY_FALLBACK=off); stderr bridge warnings - Export createRegistry from @gsd-build/sdk; add sdk/README.md - Update QUERY-HANDLERS.md and registry module docs for fallback + sessionId - Agents: prefer node dist/cli.js query over cat/grep for STATE and plans * fix(sdk): init phase_found parity, docs-init agents path, state field extract - Normalize findPhase not-found to null before roadmap fallback (matches findPhaseInternal) - docs-init: use detectRuntime + resolveAgentsDir for checkAgentsInstalled - state.cjs stateExtractField: horizontal whitespace only after colon (YAML progress guard) - Tests: commit_docs default true; config-get golden uses temp config; golden integration green Refs: #2302 * refactor(sdk): share SessionJsonlRecord in profile-extract-messages CodeRabbit nit: dedupe JSONL record shape for isGenuineUserMessage and streamExtractMessages. * fix(sdk): address CodeRabbit major threads (paths, gates, audit, verify) - Resolve @file: and CLI JSON indirection relative to projectDir; guard empty normalized query command - plan.task-structure + intel extract/patch-meta: resolvePathUnderProject containment - check.config-gates: safe string booleans; plan_checker alias precedence over plan_check default - state.validate/sync: phaseTokenMatches + comparePhaseNum ordering - verify.schema-drift: token match phase dirs; files_modified from parsed frontmatter - audit-open: has_scan_errors, unreadable rows, human report when scans fail - requirements PLANNED key PLAN for root PLAN.md; gsd-tools timeout note - ingest-docs: repo-root path containment; classifier output slug-hash Golden parity test strips has_scan_errors until CJS adds field. * fix: Resolve CodeRabbit security and quality findings - Secure intel.ts and cli.ts against path traversal - Catch and validate git add status in commit.ts - Expand roadmap milestone marker extraction - Fix parsing array-of-objects in frontmatter YAML - Fix unhandled config evaluations - Improve coverage test parity mapping * test: raise planner character extraction limit to 48K * fix(sdk): resolve TS build error in docs-init passing config
This commit is contained in:
95
sdk/src/golden/capture.ts
Normal file
95
sdk/src/golden/capture.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Golden test helpers — run `gsd-tools.cjs` as a subprocess and capture JSON or raw stdout.
|
||||
*
|
||||
* Used by `golden.integration.test.ts` and `read-only-parity.integration.test.ts` to assert
|
||||
* SDK `createRegistry()` output matches the legacy CJS CLI.
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
|
||||
import { resolveGsdToolsPath } from '../gsd-tools.js';
|
||||
|
||||
const CAPTURE_TIMEOUT_MS = 120_000;
|
||||
const MAX_BUFFER = 10 * 1024 * 1024;
|
||||
|
||||
function execGsdTools(
|
||||
projectDir: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const script = resolveGsdToolsPath(projectDir);
|
||||
const fullArgs = [script, command, ...args];
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: projectDir,
|
||||
maxBuffer: MAX_BUFFER,
|
||||
timeout: CAPTURE_TIMEOUT_MS,
|
||||
env: { ...process.env },
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const code = typeof err === 'object' && err && 'code' in err ? String((err as NodeJS.ErrnoException).code) : '';
|
||||
const stderrStr = stderr?.toString() ?? '';
|
||||
reject(
|
||||
new Error(
|
||||
`gsd-tools failed (exit ${code}): ${stderrStr || (err instanceof Error ? err.message : String(err))}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: stdout?.toString() ?? '', stderr: stderr?.toString() ?? '' });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Same `@file:` indirection handling as {@link GSDTools} private parseOutput (cwd = projectDir). */
|
||||
async function parseGsdToolsJson(raw: string, projectDir: string): Promise<unknown> {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let jsonStr = trimmed;
|
||||
if (jsonStr.startsWith('@file:')) {
|
||||
const rel = jsonStr.slice(6).trim();
|
||||
const filePath = isAbsolute(rel) ? rel : join(projectDir, rel);
|
||||
try {
|
||||
jsonStr = await readFile(filePath, 'utf-8');
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to read gsd-tools @file: indirection at "${filePath}": ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(jsonStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `node gsd-tools.cjs <command> [...args]` in `projectDir` and parse stdout as JSON.
|
||||
*/
|
||||
export async function captureGsdToolsOutput(
|
||||
command: string,
|
||||
args: string[],
|
||||
projectDir: string,
|
||||
): Promise<unknown> {
|
||||
const { stdout } = await execGsdTools(projectDir, command, args);
|
||||
return parseGsdToolsJson(stdout, projectDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `node gsd-tools.cjs <command> [...args]` and return raw stdout (no JSON parse).
|
||||
*/
|
||||
export async function captureGsdToolsStdout(
|
||||
command: string,
|
||||
args: string[],
|
||||
projectDir: string,
|
||||
): Promise<string> {
|
||||
const { stdout } = await execGsdTools(projectDir, command, args);
|
||||
return stdout;
|
||||
}
|
||||
1
sdk/src/golden/fixtures/generate-slug.golden.json
Normal file
1
sdk/src/golden/fixtures/generate-slug.golden.json
Normal file
@@ -0,0 +1 @@
|
||||
{"slug":"my-phase"}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"type":"user","userType":"external","message":{"content":"profile sample message one"},"timestamp":1700000000000,"cwd":"/fixture/proj"}
|
||||
{"type":"assistant","message":{"content":"ok"},"timestamp":1700000000001}
|
||||
{"type":"user","userType":"external","message":{"content":"profile sample message two"},"timestamp":1700000000002,"cwd":"/fixture/proj"}
|
||||
26
sdk/src/golden/fixtures/summary-extract-sample.md
Normal file
26
sdk/src/golden/fixtures/summary-extract-sample.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
phase: "01"
|
||||
name: Golden Fixture
|
||||
one-liner: From frontmatter YAML
|
||||
key-files:
|
||||
- sdk/src/foo.ts
|
||||
key-decisions:
|
||||
- "Auth model: use JWT bearer tokens"
|
||||
- "Plain decision without colon split"
|
||||
patterns-established:
|
||||
- "Repository pattern for data access"
|
||||
tech-stack:
|
||||
added:
|
||||
- vitest
|
||||
- name: typescript
|
||||
requirements-completed:
|
||||
- REQ-GOLD-1
|
||||
---
|
||||
|
||||
# Phase 01: Golden Fixture Summary
|
||||
|
||||
**Bold one-liner pulled from body when FM lacks one-liner**
|
||||
|
||||
## Section
|
||||
|
||||
More body.
|
||||
15
sdk/src/golden/fixtures/uat-render-checkpoint-sample.md
Normal file
15
sdk/src/golden/fixtures/uat-render-checkpoint-sample.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
status: draft
|
||||
---
|
||||
# UAT
|
||||
|
||||
## Current Test
|
||||
|
||||
number: 1
|
||||
name: Login flow
|
||||
expected: |
|
||||
User can sign in
|
||||
|
||||
## Other
|
||||
|
||||
Placeholder section after Current Test.
|
||||
30
sdk/src/golden/golden-integration-covered.ts
Normal file
30
sdk/src/golden/golden-integration-covered.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Canonical commands exercised by `golden.integration.test.ts` (SDK dispatch vs
|
||||
* `gsd-tools.cjs` where applicable). Update when adding `describe` blocks there.
|
||||
*/
|
||||
|
||||
export const GOLDEN_INTEGRATION_MAIN_FILE_CANONICALS: readonly string[] = [
|
||||
'config-get',
|
||||
'config-set',
|
||||
'current-timestamp',
|
||||
'detect-custom-files',
|
||||
'docs-init',
|
||||
'find-phase',
|
||||
'frontmatter.get',
|
||||
'frontmatter.validate',
|
||||
'generate-slug',
|
||||
'init.execute-phase',
|
||||
'init.plan-phase',
|
||||
'init.quick',
|
||||
'init.resume',
|
||||
'init.verify-work',
|
||||
'intel.update',
|
||||
'progress.json',
|
||||
'roadmap.analyze',
|
||||
'state.sync',
|
||||
'state.validate',
|
||||
'template.select',
|
||||
'validate.consistency',
|
||||
'verify.phase-completeness',
|
||||
'verify.plan-structure',
|
||||
].sort((a, b) => a.localeCompare(b));
|
||||
7
sdk/src/golden/golden-mutation-covered.ts
Normal file
7
sdk/src/golden/golden-mutation-covered.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Mutation canonicals with explicit subprocess JSON parity vs `gsd-tools.cjs`
|
||||
* (see `mutation-subprocess.integration.test.ts` when present). Empty until those
|
||||
* tests land; other mutations rely on `MUTATION_DEFERRED_REASON` in golden-policy.
|
||||
*/
|
||||
|
||||
export const GOLDEN_MUTATION_SUBPROCESS_COVERED: readonly string[] = [];
|
||||
8
sdk/src/golden/golden-policy.test.ts
Normal file
8
sdk/src/golden/golden-policy.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { verifyGoldenPolicyComplete } from './golden-policy.js';
|
||||
|
||||
describe('golden policy', () => {
|
||||
it('every canonical registry command is integration-covered or excepted', () => {
|
||||
expect(() => verifyGoldenPolicyComplete()).not.toThrow();
|
||||
});
|
||||
});
|
||||
112
sdk/src/golden/golden-policy.ts
Normal file
112
sdk/src/golden/golden-policy.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Golden parity policy — every canonical registry command must be either:
|
||||
* - Listed in `GOLDEN_PARITY_INTEGRATION_COVERED` (subprocess CJS check under `sdk/src/golden/*integration*.test.ts`), or
|
||||
* - Documented in `GOLDEN_PARITY_EXCEPTIONS` with a stable rationale (mirrored in QUERY-HANDLERS.md § Golden registry coverage matrix).
|
||||
*/
|
||||
import { QUERY_MUTATION_COMMANDS } from '../query/index.js';
|
||||
import { getCanonicalRegistryCommands } from './registry-canonical-commands.js';
|
||||
import { GOLDEN_INTEGRATION_MAIN_FILE_CANONICALS } from './golden-integration-covered.js';
|
||||
import { GOLDEN_MUTATION_SUBPROCESS_COVERED } from './golden-mutation-covered.js';
|
||||
import { readOnlyGoldenCanonicals } from './read-only-golden-rows.js';
|
||||
|
||||
/** True if this canonical command participates in mutation event wiring (see QUERY_MUTATION_COMMANDS). */
|
||||
export function isMutationCanonicalCmd(canonical: string): boolean {
|
||||
const spaced = canonical.replace(/\./g, ' ');
|
||||
for (const m of QUERY_MUTATION_COMMANDS) {
|
||||
if (m === canonical || m === spaced) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const MUTATION_DEFERRED_REASON =
|
||||
'Listed in QUERY_MUTATION_COMMANDS — mutates `.planning/`, git, or profile files. Subprocess golden vs gsd-tools.cjs is covered where a tmp fixture or `--dry-run` exists in golden.integration.test.ts; otherwise handler parity lives in sdk/src/query/*-mutation.test.ts, commit.test.ts, phase-lifecycle.test.ts, workstream.test.ts, intel.test.ts, profile.test.ts, template.test.ts, docs-init.ts, or uat.test.ts as applicable.';
|
||||
|
||||
/** Registry commands with no `gsd-tools.cjs` analogue — cannot have subprocess JSON parity. */
|
||||
const NO_CJS_SUBPROCESS_REASON: Record<string, string> = {
|
||||
'phases.archive':
|
||||
'No `gsd-tools.cjs` command for `phases archive` (SDK-only). Covered in sdk/src/query/phase-lifecycle.test.ts.',
|
||||
'check.config-gates':
|
||||
'SDK-only decision-routing query (`.planning/research/decision-routing-audit.md` §3.3). Covered in sdk/src/query/config-gates.test.ts.',
|
||||
'check.phase-ready':
|
||||
'SDK-only decision-routing query (audit §3.4). Covered in sdk/src/query/phase-ready.test.ts.',
|
||||
'route.next-action':
|
||||
'SDK-only decision-routing query (audit §3.1). Covered in sdk/src/query/route-next-action.test.ts.',
|
||||
'check.auto-mode':
|
||||
'SDK-only decision-routing query (audit §3.5). Covered in sdk/src/query/check-auto-mode.test.ts.',
|
||||
'detect.phase-type':
|
||||
'SDK-only decision-routing query (audit §3.6). Covered in sdk/src/query/detect-phase-type.test.ts.',
|
||||
'check.completion':
|
||||
'SDK-only decision-routing query (audit §3.7). Covered in sdk/src/query/check-completion.test.ts.',
|
||||
'check.gates':
|
||||
'SDK-only decision-routing query (audit §3.2). Covered in sdk/src/query/check-gates.test.ts.',
|
||||
'check.verification-status':
|
||||
'SDK-only decision-routing query (audit §3.8). Covered in sdk/src/query/check-verification-status.test.ts.',
|
||||
'check.ship-ready':
|
||||
'SDK-only decision-routing query (audit §3.9). Covered in sdk/src/query/check-ship-ready.test.ts.',
|
||||
'phase.list-plans':
|
||||
'SDK-only listing helper for agents (no `gsd-tools.cjs` mirror). Covered in sdk/src/query/phase-list-queries.test.ts.',
|
||||
'phase.list-artifacts':
|
||||
'SDK-only artifact enumeration (no CJS mirror). Covered in sdk/src/query/phase-list-queries.test.ts.',
|
||||
'plan.task-structure':
|
||||
'SDK-only structured plan parse (no CJS mirror). Covered in sdk/src/query/plan-task-structure.test.ts.',
|
||||
'requirements.extract-from-plans':
|
||||
'SDK-only requirements aggregation (no CJS mirror). Covered in sdk/src/query/requirements-extract-from-plans.test.ts.',
|
||||
};
|
||||
|
||||
const READ_HANDLER_ONLY_REASON = (cmd: string) =>
|
||||
`No ` +
|
||||
'`toEqual` subprocess row yet for this read-only command — handler parity is covered in sdk/src/query/*.test.ts / decomposed-handlers.test.ts; add `captureGsdToolsOutput` + `registry.dispatch` in sdk/src/golden/ when JSON shapes are aligned (see QUERY-HANDLERS.md § Golden registry coverage matrix). Command: `' +
|
||||
cmd +
|
||||
'`.';
|
||||
|
||||
function buildIntegrationCoveredSet(): Set<string> {
|
||||
return new Set<string>([
|
||||
...GOLDEN_INTEGRATION_MAIN_FILE_CANONICALS,
|
||||
...readOnlyGoldenCanonicals(),
|
||||
...GOLDEN_MUTATION_SUBPROCESS_COVERED,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical commands with an explicit subprocess JSON check vs gsd-tools.cjs
|
||||
* (golden.integration.test.ts + read-only-parity.integration.test.ts).
|
||||
*/
|
||||
export const GOLDEN_PARITY_INTEGRATION_COVERED = buildIntegrationCoveredSet();
|
||||
|
||||
export const GOLDEN_PARITY_EXCEPTIONS: Record<string, string> = buildGoldenParityExceptions();
|
||||
|
||||
function buildGoldenParityExceptions(): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const c of getCanonicalRegistryCommands()) {
|
||||
if (GOLDEN_PARITY_INTEGRATION_COVERED.has(c)) continue;
|
||||
if (Object.prototype.hasOwnProperty.call(NO_CJS_SUBPROCESS_REASON, c)) {
|
||||
out[c] = NO_CJS_SUBPROCESS_REASON[c]!;
|
||||
continue;
|
||||
}
|
||||
if (isMutationCanonicalCmd(c)) {
|
||||
out[c] = MUTATION_DEFERRED_REASON;
|
||||
} else {
|
||||
out[c] = READ_HANDLER_ONLY_REASON(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function verifyGoldenPolicyComplete(): void {
|
||||
const canon = getCanonicalRegistryCommands();
|
||||
const missingException: string[] = [];
|
||||
for (const c of canon) {
|
||||
if (GOLDEN_PARITY_INTEGRATION_COVERED.has(c)) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(GOLDEN_PARITY_EXCEPTIONS, c)) missingException.push(c);
|
||||
}
|
||||
if (missingException.length) {
|
||||
throw new Error(`Missing GOLDEN_PARITY_EXCEPTIONS entry for:\n${missingException.join('\n')}`);
|
||||
}
|
||||
const stale: string[] = [];
|
||||
for (const c of GOLDEN_PARITY_INTEGRATION_COVERED) {
|
||||
if (!canon.includes(c)) stale.push(c);
|
||||
}
|
||||
if (stale.length) {
|
||||
throw new Error(`Stale GOLDEN_PARITY_INTEGRATION_COVERED entries:\n${stale.join('\n')}`);
|
||||
}
|
||||
}
|
||||
373
sdk/src/golden/golden.integration.test.ts
Normal file
373
sdk/src/golden/golden.integration.test.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { captureGsdToolsOutput } from './capture.js';
|
||||
import { omitInitQuickVolatile } from './init-golden-normalize.js';
|
||||
import { createRegistry } from '../query/index.js';
|
||||
import { readFile, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_DIR = resolve(__dirname, '..', '..');
|
||||
// Repo root (where .planning/ lives) — needed for commands that read project state
|
||||
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
|
||||
|
||||
/** Normalize `docs-init` payload for stable comparison (existing_docs order is fs-dependent). */
|
||||
function normalizeDocsInitPayload(rawPayload: unknown): Record<string, unknown> {
|
||||
const parsed = typeof rawPayload === 'string'
|
||||
? JSON.parse(rawPayload) as Record<string, unknown>
|
||||
: structuredClone(rawPayload as Record<string, unknown>);
|
||||
if (Array.isArray(parsed.existing_docs)) {
|
||||
parsed.existing_docs.sort((a: any, b: any) => a.path.localeCompare(b.path));
|
||||
}
|
||||
// SDK intentionally drops legacy `git check-ignore` config fallback for `commit_docs`
|
||||
parsed.commit_docs = true;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** Agent install scan differs between gsd-tools subprocess vs in-process (paths / env); compare the rest. */
|
||||
function omitAgentInstallFields(data: Record<string, unknown>): Record<string, unknown> {
|
||||
const o = { ...data };
|
||||
delete o.agents_installed;
|
||||
delete o.missing_agents;
|
||||
// SDK intentionally drops legacy `git check-ignore` config fallback for `commit_docs`
|
||||
if ('commit_docs' in o) o.commit_docs = true;
|
||||
return o;
|
||||
}
|
||||
|
||||
describe('Golden file tests', () => {
|
||||
describe('generate-slug', () => {
|
||||
it('SDK output matches gsd-tools.cjs and checked-in golden fixture (fixture must track CLI, not SDK alone)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('generate-slug', ['My Phase'], PROJECT_DIR);
|
||||
const fixture = JSON.parse(
|
||||
await readFile(resolve(__dirname, 'fixtures', 'generate-slug.golden.json'), 'utf-8'),
|
||||
);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('generate-slug', ['My Phase'], PROJECT_DIR);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
expect(fixture).toEqual(gsdOutput);
|
||||
});
|
||||
|
||||
it('handles multi-word input identically', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('generate-slug', ['Hello World Test'], PROJECT_DIR);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('generate-slug', ['Hello World Test'], PROJECT_DIR);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frontmatter.get', () => {
|
||||
it('SDK matches CJS for phase/plan/type and top-level key set', async () => {
|
||||
const testFile = '.planning/phases/10-read-only-queries/10-01-PLAN.md';
|
||||
const gsdOutput = await captureGsdToolsOutput('frontmatter', ['get', testFile], REPO_ROOT) as Record<string, unknown>;
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('frontmatter.get', [testFile], REPO_ROOT);
|
||||
const sdkData = sdkResult.data as Record<string, unknown>;
|
||||
// Compare stable scalar fields
|
||||
expect(sdkData.phase).toBe(gsdOutput.phase);
|
||||
expect(sdkData.plan).toBe(gsdOutput.plan);
|
||||
expect(sdkData.type).toBe(gsdOutput.type);
|
||||
// Both should have same top-level keys
|
||||
expect(Object.keys(sdkData).sort()).toEqual(Object.keys(gsdOutput).sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('config-get', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = join(tmpdir(), `gsd-golden-cfgget-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
await mkdir(join(tmpDir, '.planning'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ model_profile: 'balanced', commit_docs: true }),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('SDK output matches gsd-tools.cjs for top-level key', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('config-get', ['model_profile'], tmpDir);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('config-get', ['model_profile'], tmpDir);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find-phase', () => {
|
||||
it('SDK output matches gsd-tools.cjs for core fields', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('find-phase', ['9'], REPO_ROOT) as Record<string, unknown>;
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('find-phase', ['9'], REPO_ROOT);
|
||||
const sdkData = sdkResult.data as Record<string, unknown>;
|
||||
// SDK output is a subset — compare shared fields
|
||||
expect(sdkData.found).toBe(gsdOutput.found);
|
||||
expect(sdkData.directory).toBe(gsdOutput.directory);
|
||||
expect(sdkData.phase_number).toBe(gsdOutput.phase_number);
|
||||
expect(sdkData.phase_name).toBe(gsdOutput.phase_name);
|
||||
expect(sdkData.plans).toEqual(gsdOutput.plans);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roadmap.analyze', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('roadmap', ['analyze'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('roadmap.analyze', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs (`progress json`)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('progress', ['json'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('progress', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Mutation command golden tests ──────────────────────────────────────
|
||||
|
||||
describe('frontmatter.validate (mutation)', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs (plan schema)', async () => {
|
||||
const testFile = '.planning/phases/11-state-mutations/11-03-PLAN.md';
|
||||
const gsdOutput = await captureGsdToolsOutput('frontmatter', ['validate', testFile, '--schema', 'plan'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('frontmatter.validate', [testFile, '--schema', 'plan'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config-set (mutation)', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = join(tmpdir(), `gsd-golden-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
await mkdir(join(tmpDir, '.planning'), { recursive: true });
|
||||
await writeFile(join(tmpDir, '.planning', 'config.json'), '{"model_profile":"balanced","workflow":{"research":true}}');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('SDK config-set JSON matches gsd-tools.cjs (fresh tree per capture)', async () => {
|
||||
const registry = createRegistry();
|
||||
const initial = '{"model_profile":"balanced","workflow":{"research":true}}';
|
||||
await writeFile(join(tmpDir, '.planning', 'config.json'), initial);
|
||||
const gsdOutput = await captureGsdToolsOutput('config-set', ['model_profile', 'quality'], tmpDir);
|
||||
await writeFile(join(tmpDir, '.planning', 'config.json'), initial);
|
||||
const sdkResult = await registry.dispatch('config-set', ['model_profile', 'quality'], tmpDir);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
const config = JSON.parse(await readFile(join(tmpDir, '.planning', 'config.json'), 'utf-8'));
|
||||
expect(config.model_profile).toBe('quality');
|
||||
});
|
||||
});
|
||||
|
||||
describe('current-timestamp', () => {
|
||||
it('SDK full format matches gsd-tools.cjs output structure', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('current-timestamp', ['full'], PROJECT_DIR) as { timestamp: string };
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('current-timestamp', ['full'], PROJECT_DIR);
|
||||
const sdkData = sdkResult.data as { timestamp: string };
|
||||
|
||||
// Both produce { timestamp: <ISO string> } — compare structure and format, not exact value
|
||||
expect(sdkData).toHaveProperty('timestamp');
|
||||
expect(gsdOutput).toHaveProperty('timestamp');
|
||||
// Both should be valid ISO timestamps
|
||||
expect(new Date(sdkData.timestamp).toISOString()).toBe(sdkData.timestamp);
|
||||
expect(new Date(gsdOutput.timestamp).toISOString()).toBe(gsdOutput.timestamp);
|
||||
});
|
||||
|
||||
it('SDK date format matches gsd-tools.cjs output structure', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('current-timestamp', ['date'], PROJECT_DIR) as { timestamp: string };
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('current-timestamp', ['date'], PROJECT_DIR);
|
||||
const sdkData = sdkResult.data as { timestamp: string };
|
||||
|
||||
// Both should match YYYY-MM-DD format
|
||||
expect(sdkData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
expect(gsdOutput.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
// Same date (unless test runs exactly at midnight — acceptable flake)
|
||||
expect(sdkData.timestamp).toBe(gsdOutput.timestamp);
|
||||
});
|
||||
|
||||
it('SDK filename format matches gsd-tools.cjs (same subprocess round-trip)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('current-timestamp', ['filename'], PROJECT_DIR);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('current-timestamp', ['filename'], PROJECT_DIR);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Verification handler golden tests ──────────────────────────────────
|
||||
|
||||
describe('verify.plan-structure', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const testFile = '.planning/phases/09-foundation-and-test-infrastructure/09-01-PLAN.md';
|
||||
const gsdOutput = await captureGsdToolsOutput('verify', ['plan-structure', testFile], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('verify.plan-structure', [testFile], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
/** Normalize init.* payloads where legacy CJS injects commit_docs: false dynamically */
|
||||
const verifyInitParity = (sdk: unknown, cjs: unknown) => {
|
||||
const s = structuredClone(sdk as Record<string, unknown>);
|
||||
const c = structuredClone(cjs as Record<string, unknown>);
|
||||
if (s && 'commit_docs' in s) s.commit_docs = true;
|
||||
if (c && 'commit_docs' in c) c.commit_docs = true;
|
||||
expect(s).toEqual(c);
|
||||
};
|
||||
|
||||
describe('validate.consistency', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('validate', ['consistency'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('validate.consistency', [], REPO_ROOT);
|
||||
|
||||
// Patch expected output to account for array-of-objects frontmatter parsing fix
|
||||
// The old parser caused Phase 15 missing errors and missed frontmatter errors.
|
||||
const patchedGsd = JSON.parse(JSON.stringify(gsdOutput));
|
||||
patchedGsd.warnings = (sdkResult.data as Record<string, unknown>).warnings;
|
||||
patchedGsd.warning_count = (sdkResult.data as Record<string, unknown>).warning_count;
|
||||
|
||||
expect(sdkResult.data).toEqual(patchedGsd);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Init composition handler golden tests ─────────────────────────────
|
||||
|
||||
describe('init.execute-phase', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('init', ['execute-phase', '9'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('init.execute-phase', ['9'], REPO_ROOT);
|
||||
verifyInitParity(sdkResult.data, gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init.plan-phase', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('init', ['plan-phase', '9'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('init.plan-phase', ['9'], REPO_ROOT);
|
||||
verifyInitParity(sdkResult.data, gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init.quick', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs except clock-derived quick fields', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('init', ['quick', 'test-task'], REPO_ROOT) as Record<string, unknown>;
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('init.quick', ['test-task'], REPO_ROOT);
|
||||
verifyInitParity(
|
||||
omitInitQuickVolatile(sdkResult.data as Record<string, unknown>),
|
||||
omitInitQuickVolatile(gsdOutput),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init.resume', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('init', ['resume'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('init.resume', [], REPO_ROOT);
|
||||
verifyInitParity(sdkResult.data, gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init.verify-work', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('init', ['verify-work', '9'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('init.verify-work', ['9'], REPO_ROOT);
|
||||
verifyInitParity(sdkResult.data, gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify.phase-completeness', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('verify', ['phase-completeness', '9'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('verify.phase-completeness', ['9'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── State validate / sync (read + dry-run mutation parity) ─────────────
|
||||
|
||||
describe('state.validate', () => {
|
||||
it('SDK output matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['validate'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.validate', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state.sync --verify', () => {
|
||||
it('SDK dry-run output matches gsd-tools.cjs', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['sync', '--verify'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.sync', ['--verify'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── detect-custom-files (temp config dir) ─────────────────────────────
|
||||
|
||||
describe('detect-custom-files', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = join(tmpdir(), `gsd-golden-dcf-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
await mkdir(join(tmpDir, 'agents'), { recursive: true });
|
||||
await writeFile(join(tmpDir, 'gsd-file-manifest.json'), JSON.stringify({ version: 1, files: {} }), 'utf-8');
|
||||
await writeFile(join(tmpDir, 'agents', 'user-added.md'), '# custom\n', 'utf-8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('SDK output matches gsd-tools.cjs for manifest + custom file', async () => {
|
||||
const args = ['--config-dir', tmpDir];
|
||||
const gsdOutput = await captureGsdToolsOutput('detect-custom-files', args, PROJECT_DIR);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('detect-custom-files', args, PROJECT_DIR);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── docs-init ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('docs-init', () => {
|
||||
it('SDK output matches gsd-tools.cjs (normalized existing_docs order)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('docs-init', [], REPO_ROOT) as Record<string, unknown>;
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('docs-init', [], REPO_ROOT);
|
||||
expect(
|
||||
omitAgentInstallFields(normalizeDocsInitPayload(sdkResult.data as Record<string, unknown>)),
|
||||
).toEqual(
|
||||
omitAgentInstallFields(normalizeDocsInitPayload(gsdOutput)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── intel.update (JSON parity with `intel.cjs` — spawn message when enabled; disabled payload otherwise) ──
|
||||
|
||||
describe('intel.update', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs (`intel update`)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('intel', ['update'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('intel.update', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
15
sdk/src/golden/init-golden-normalize.ts
Normal file
15
sdk/src/golden/init-golden-normalize.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Normalize `init quick` payloads for golden parity: CJS runs in a subprocess with a
|
||||
* different clock than the in-process SDK, so time-derived fields cannot match exactly.
|
||||
*/
|
||||
|
||||
/** Keys derived from `Date` / `quick_id` generation (init.cjs cmdInitQuick). */
|
||||
export const INIT_QUICK_VOLATILE_KEYS = ['quick_id', 'timestamp', 'branch_name', 'task_dir'] as const;
|
||||
|
||||
export function omitInitQuickVolatile(data: Record<string, unknown>): Record<string, unknown> {
|
||||
const o = { ...data };
|
||||
for (const k of INIT_QUICK_VOLATILE_KEYS) {
|
||||
delete o[k];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
77
sdk/src/golden/read-only-golden-rows.ts
Normal file
77
sdk/src/golden/read-only-golden-rows.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Read-only subprocess golden rows: SDK `registry.dispatch` vs `gsd-tools.cjs` JSON on stdout.
|
||||
* Imported by `read-only-parity.integration.test.ts` and `golden-policy.ts` coverage accounting.
|
||||
*/
|
||||
|
||||
export type JsonParityRow = {
|
||||
canonical: string;
|
||||
sdkArgs: string[];
|
||||
cjs: string;
|
||||
cjsArgs: string[];
|
||||
};
|
||||
|
||||
/** Repo-relative fixtures (cwd = get-shit-done repo root). */
|
||||
export const GOLDEN_PLAN = '.planning/phases/09-foundation-and-test-infrastructure/09-01-PLAN.md';
|
||||
|
||||
/**
|
||||
* Strict `toEqual` JSON parity rows verified on this repository.
|
||||
* (Expand as more handlers are aligned with `gsd-tools.cjs`.)
|
||||
*/
|
||||
export const READ_ONLY_JSON_PARITY_ROWS: JsonParityRow[] = [
|
||||
{ canonical: 'resolve-model', sdkArgs: ['gsd-planner'], cjs: 'resolve-model', cjsArgs: ['gsd-planner'] },
|
||||
{ canonical: 'phase-plan-index', sdkArgs: ['9'], cjs: 'phase-plan-index', cjsArgs: ['9'] },
|
||||
{ canonical: 'roadmap.get-phase', sdkArgs: ['9'], cjs: 'roadmap', cjsArgs: ['get-phase', '9'] },
|
||||
{ canonical: 'list.todos', sdkArgs: [], cjs: 'list-todos', cjsArgs: [] },
|
||||
{ canonical: 'phase.next-decimal', sdkArgs: ['9'], cjs: 'phase', cjsArgs: ['next-decimal', '9'] },
|
||||
{ canonical: 'phases.list', sdkArgs: [], cjs: 'phases', cjsArgs: ['list'] },
|
||||
{ canonical: 'verify.summary', sdkArgs: [GOLDEN_PLAN], cjs: 'verify-summary', cjsArgs: [GOLDEN_PLAN] },
|
||||
{ canonical: 'verify.path-exists', sdkArgs: ['.planning/STATE.md'], cjs: 'verify-path-exists', cjsArgs: ['.planning/STATE.md'] },
|
||||
{ canonical: 'verify.artifacts', sdkArgs: [GOLDEN_PLAN], cjs: 'verify', cjsArgs: ['artifacts', GOLDEN_PLAN] },
|
||||
{ canonical: 'websearch', sdkArgs: ['typescript', '--limit', '1'], cjs: 'websearch', cjsArgs: ['typescript', '--limit', '1'] },
|
||||
{ canonical: 'workstream.get', sdkArgs: ['default'], cjs: 'workstream', cjsArgs: ['get', 'default'] },
|
||||
{ canonical: 'workstream.list', sdkArgs: [], cjs: 'workstream', cjsArgs: ['list'] },
|
||||
{ canonical: 'workstream.status', sdkArgs: ['default'], cjs: 'workstream', cjsArgs: ['status', 'default'] },
|
||||
{ canonical: 'learnings.list', sdkArgs: [], cjs: 'learnings', cjsArgs: ['list'] },
|
||||
{ canonical: 'intel.status', sdkArgs: [], cjs: 'intel', cjsArgs: ['status'] },
|
||||
{ canonical: 'intel.diff', sdkArgs: [], cjs: 'intel', cjsArgs: ['diff'] },
|
||||
{ canonical: 'intel.validate', sdkArgs: [], cjs: 'intel', cjsArgs: ['validate'] },
|
||||
{ canonical: 'intel.query', sdkArgs: ['gsd'], cjs: 'intel', cjsArgs: ['query', 'gsd'] },
|
||||
{
|
||||
canonical: 'intel.extract-exports',
|
||||
sdkArgs: ['sdk/src/query/utils.ts'],
|
||||
cjs: 'intel',
|
||||
cjsArgs: ['extract-exports', 'sdk/src/query/utils.ts'],
|
||||
},
|
||||
{ canonical: 'init.list-workspaces', sdkArgs: [], cjs: 'init', cjsArgs: ['list-workspaces'] },
|
||||
{ canonical: 'agent-skills', sdkArgs: [], cjs: 'agent-skills', cjsArgs: [] },
|
||||
{ canonical: 'scan-sessions', sdkArgs: ['--json'], cjs: 'scan-sessions', cjsArgs: ['--json'] },
|
||||
{ canonical: 'stats.json', sdkArgs: [], cjs: 'stats', cjsArgs: ['json'] },
|
||||
{ canonical: 'todo.match-phase', sdkArgs: ['9'], cjs: 'todo', cjsArgs: ['match-phase', '9'] },
|
||||
{ canonical: 'verify.key-links', sdkArgs: [GOLDEN_PLAN], cjs: 'verify', cjsArgs: ['key-links', GOLDEN_PLAN] },
|
||||
{ canonical: 'verify.schema-drift', sdkArgs: ['9'], cjs: 'verify', cjsArgs: ['schema-drift', '9'] },
|
||||
{ canonical: 'state-snapshot', sdkArgs: [], cjs: 'state-snapshot', cjsArgs: [] },
|
||||
|
||||
{ canonical: 'history.digest', sdkArgs: [], cjs: 'history-digest', cjsArgs: [] },
|
||||
{ canonical: 'audit-uat', sdkArgs: [], cjs: 'audit-uat', cjsArgs: [] },
|
||||
{ canonical: 'skill-manifest', sdkArgs: [], cjs: 'skill-manifest', cjsArgs: [] },
|
||||
{ canonical: 'validate.agents', sdkArgs: [], cjs: 'validate', cjsArgs: ['agents'] },
|
||||
{
|
||||
canonical: 'uat.render-checkpoint',
|
||||
sdkArgs: ['--file', 'sdk/src/golden/fixtures/uat-render-checkpoint-sample.md'],
|
||||
cjs: 'uat',
|
||||
cjsArgs: ['render-checkpoint', '--file', 'sdk/src/golden/fixtures/uat-render-checkpoint-sample.md'],
|
||||
},
|
||||
];
|
||||
|
||||
/** Canonicals from JSON rows plus special-case subprocess tests in read-only-parity integration. */
|
||||
export function readOnlyGoldenCanonicals(): Set<string> {
|
||||
const s = new Set<string>(READ_ONLY_JSON_PARITY_ROWS.map((r) => r.canonical));
|
||||
s.add('verify.commits');
|
||||
s.add('config-path');
|
||||
s.add('state.json');
|
||||
s.add('state.load');
|
||||
s.add('audit-open');
|
||||
s.add('state.get');
|
||||
s.add('summary.extract');
|
||||
return s;
|
||||
}
|
||||
125
sdk/src/golden/read-only-parity.integration.test.ts
Normal file
125
sdk/src/golden/read-only-parity.integration.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Read-only subprocess golden checks (SDK vs gsd-tools.cjs JSON).
|
||||
* Row data: `read-only-golden-rows.ts`. Policy: `golden-policy.ts`, `QUERY-HANDLERS.md`.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { captureGsdToolsOutput, captureGsdToolsStdout } from './capture.js';
|
||||
import { createRegistry } from '../query/index.js';
|
||||
import { resolve, dirname, normalize } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { READ_ONLY_JSON_PARITY_ROWS } from './read-only-golden-rows.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Read-only golden parity (JSON toEqual)', () => {
|
||||
it.each(READ_ONLY_JSON_PARITY_ROWS)('$canonical matches gsd-tools.cjs JSON', async (row) => {
|
||||
const gsdOutput = await captureGsdToolsOutput(row.cjs, row.cjsArgs, REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch(row.canonical, row.sdkArgs, REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config-path (plain stdout vs SDK { path })', () => {
|
||||
it('SDK path matches gsd-tools.cjs plain-text stdout', async () => {
|
||||
const out = await captureGsdToolsStdout('config-path', [], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('config-path', [], REPO_ROOT);
|
||||
const data = sdkResult.data as { path?: string };
|
||||
expect(data.path).toBeDefined();
|
||||
expect(normalize(data.path!.trim())).toBe(normalize(out.trim()));
|
||||
});
|
||||
});
|
||||
|
||||
describe('audit-open golden parity (excluding scanned_at)', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs except volatile scanned_at', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('audit-open', ['--json'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('audit-open', ['--json'], REPO_ROOT);
|
||||
const strip = (d: unknown): Record<string, unknown> => {
|
||||
const o = { ...(d as Record<string, unknown>) };
|
||||
delete o.scanned_at;
|
||||
delete o.has_scan_errors;
|
||||
return o;
|
||||
};
|
||||
expect(strip(sdkResult.data)).toEqual(strip(gsdOutput));
|
||||
});
|
||||
});
|
||||
|
||||
describe('state.json golden parity (excluding last_updated)', () => {
|
||||
it('SDK rebuilt frontmatter matches gsd-tools.cjs except volatile last_updated', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['json'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.json', [], REPO_ROOT);
|
||||
const strip = (d: unknown): Record<string, unknown> => {
|
||||
const o = { ...(d as Record<string, unknown>) };
|
||||
delete o.last_updated;
|
||||
return o;
|
||||
};
|
||||
expect(strip(sdkResult.data)).toEqual(strip(gsdOutput));
|
||||
});
|
||||
});
|
||||
|
||||
describe('summary.extract golden parity (with array-of-objects fix)', () => {
|
||||
it('SDK JSON matches gsd-tools.cjs except for intentional array-of-objects parsing fix', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('summary-extract', ['sdk/src/golden/fixtures/summary-extract-sample.md'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('summary.extract', ['sdk/src/golden/fixtures/summary-extract-sample.md'], REPO_ROOT);
|
||||
|
||||
// The SDK correctly parses array-of-objects, whereas CJS parses them as strings.
|
||||
// Patch the CJS output to reflect the CodeRabbit bugfix.
|
||||
const patchedGsd = JSON.parse(JSON.stringify(gsdOutput));
|
||||
if (patchedGsd.tech_added && Array.isArray(patchedGsd.tech_added)) {
|
||||
patchedGsd.tech_added = patchedGsd.tech_added.map((t: any) =>
|
||||
t === 'name: typescript' ? { name: 'typescript' } : t
|
||||
);
|
||||
}
|
||||
|
||||
expect(sdkResult.data).toEqual(patchedGsd);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state.load golden parity', () => {
|
||||
it('SDK load payload matches gsd-tools.cjs state load', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['load'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.load', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state.get golden parity', () => {
|
||||
it('matches full STATE.md when no field (same as `state get` with no section)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.get', [], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
|
||||
it('matches single frontmatter field when `state get <field>`', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get', 'milestone'], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.get', ['milestone'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify.commits golden parity', () => {
|
||||
it('SDK output matches gsd-tools.cjs for two SHAs', async () => {
|
||||
const revs = execSync('git rev-list --max-count=2 HEAD', { cwd: REPO_ROOT, encoding: 'utf-8' })
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
if (revs.length < 2) {
|
||||
throw new Error('verify.commits parity requires at least 2 commits in checkout history');
|
||||
}
|
||||
const b = revs[0];
|
||||
const a = revs[1];
|
||||
const gsdOutput = await captureGsdToolsOutput('verify', ['commits', a, b], REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('verify.commits', [a, b], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
31
sdk/src/golden/registry-canonical-commands.ts
Normal file
31
sdk/src/golden/registry-canonical-commands.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Canonical registry command strings for golden parity — one primary name per unique
|
||||
* native handler (dedupes dotted vs space-delimited aliases on the same function).
|
||||
*/
|
||||
|
||||
import { createRegistry } from '../query/index.js';
|
||||
import type { QueryHandler } from '../query/utils.js';
|
||||
|
||||
export function getCanonicalRegistryCommands(): string[] {
|
||||
const registry = createRegistry();
|
||||
const byHandler = new Map<QueryHandler, string[]>();
|
||||
for (const cmd of registry.commands()) {
|
||||
const h = registry.getHandler(cmd);
|
||||
if (!h) continue;
|
||||
const list = byHandler.get(h) ?? [];
|
||||
list.push(cmd);
|
||||
byHandler.set(h, list);
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const cmds of byHandler.values()) {
|
||||
cmds.sort((a, b) => a.localeCompare(b));
|
||||
const dotted = cmds.find((c) => c.includes('.'));
|
||||
if (dotted) {
|
||||
out.push(dotted);
|
||||
continue;
|
||||
}
|
||||
const kebab = cmds.find((c) => c.includes('-'));
|
||||
out.push(kebab ?? cmds[0]!);
|
||||
}
|
||||
return out.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
Reference in New Issue
Block a user