* test: Bootstrapped sdk/ as TypeScript ESM package with full GSD-1 PLAN.… - "sdk/package.json" - "sdk/tsconfig.json" - "sdk/vitest.config.ts" - "sdk/src/types.ts" - "sdk/src/plan-parser.ts" - "sdk/src/plan-parser.test.ts" GSD-Task: S01/T01 * test: Implemented config reader and gsd-tools bridge with 25 unit tests… - "sdk/src/config.ts" - "sdk/src/config.test.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T02 * test: Built prompt-builder, session-runner, and GSD class — 85 total un… - "sdk/src/prompt-builder.ts" - "sdk/src/prompt-builder.test.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S01/T03 * test: Created E2E integration test with fixtures proving full SDK pipel… - "sdk/src/e2e.integration.test.ts" - "sdk/test-fixtures/sample-plan.md" - "sdk/test-fixtures/.planning/config.json" - "sdk/test-fixtures/.planning/STATE.md" - "vitest.config.ts" - "tsconfig.json" GSD-Task: S01/T04 * test: Added PhaseType/GSDEventType enums, 16-variant GSDEvent union, GS… - "sdk/src/types.ts" - "sdk/src/event-stream.ts" - "sdk/src/logger.ts" - "sdk/src/event-stream.test.ts" - "sdk/src/logger.test.ts" GSD-Task: S02/T01 * test: Built ContextEngine for phase-aware context file resolution, getT… - "sdk/src/context-engine.ts" - "sdk/src/tool-scoping.ts" - "sdk/src/phase-prompt.ts" - "sdk/src/context-engine.test.ts" - "sdk/src/tool-scoping.test.ts" - "sdk/src/phase-prompt.test.ts" GSD-Task: S02/T02 * test: Wired event stream into session runner, added onEvent()/addTransp… - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/e2e.integration.test.ts" GSD-Task: S02/T03 * feat: Added PhaseStepType enum, PhaseOpInfo interface, phase lifecycle… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S03/T01 * test: Implemented PhaseRunner state machine with 39 unit tests covering… - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S03/T02 * test: Wired PhaseRunner into GSD.runPhase() public API with full re-exp… - "sdk/src/index.ts" - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/phase-runner.ts" GSD-Task: S03/T03 * test: Expanded runVerifyStep with full gap closure cycle (plan → execut… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S04/T02 * fix: Added 3 integration tests proving phasePlanIndex returns correct t… - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/index.ts" GSD-Task: S04/T03 * test: Add milestone-level types, typed roadmapAnalyze(), GSD.run() orch… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/index.ts" - "sdk/src/milestone-runner.test.ts" GSD-Task: S05/T01 * test: Added CLITransport (structured stdout log lines) and WSTransport… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" - "sdk/src/ws-transport.ts" - "sdk/src/ws-transport.test.ts" - "sdk/src/index.ts" - "sdk/package.json" GSD-Task: S05/T02 * test: Added gsd-sdk CLI entry point with argument parsing, bin field, p… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/package.json" GSD-Task: S05/T03 * feat: Add InitNewProjectInfo type, initNewProject()/configSet() GSDTool… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T01 * chore: Created InitRunner orchestrator with setup → config → PROJECT.md… - "sdk/src/init-runner.ts" - "sdk/src/types.ts" - "sdk/src/index.ts" GSD-Task: S01/T02 * test: Wired InitRunner into CLI main() for full gsd-sdk init dispatch a… - "sdk/src/cli.ts" - "sdk/src/init-runner.test.ts" - "sdk/src/cli.test.ts" GSD-Task: S01/T03 * test: Add PlanCheck step, AI self-discuss, and retryOnce wrapper to Pha… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/session-runner.ts" - "sdk/src/phase-runner.test.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S02/T01 * feat: Rewrite CLITransport with ANSI colors, phase banners, spawn indic… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" GSD-Task: S02/T02 * test: Add `gsd-sdk auto` command with autoMode config override, USAGE t… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S02/T03 * fix: CLI shebang + gsd-tools non-JSON output handling Three bugs found during first real gsd-sdk run: 1. cli.ts shebang was commented out — shell executed JS as bash, triggering ImageMagick's import command instead of Node 2. configSet() called exec() which JSON.parse()d the output, but gsd-tools config-set returns 'key=value' text, not JSON. Added execRaw() method for commands that return plain text. 3. Same JSON parse bug affected commit() (returns git SHA), stateLoad(), verifySummary(), initExecutePhase(), stateBeginPhase(), and phaseComplete(). All switched to execRaw(). Tests updated to match real gsd-tools output format (plain text instead of mocked JSON). 376/376 tests pass.
179 lines
7.0 KiB
TypeScript
179 lines
7.0 KiB
TypeScript
/**
|
|
* E2E integration test — proves full SDK pipeline:
|
|
* parse → prompt → query() → SUMMARY.md
|
|
*
|
|
* Requires Claude Code CLI (`claude`) installed and authenticated.
|
|
* Skips gracefully if CLI is unavailable.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { execSync } from 'node:child_process';
|
|
import { mkdtemp, cp, rm, readFile, readdir } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { GSD, parsePlanFile, GSDEventType } from './index.js';
|
|
import type { GSDEvent } from './index.js';
|
|
|
|
// ─── CLI availability check ─────────────────────────────────────────────────
|
|
|
|
let cliAvailable = false;
|
|
try {
|
|
execSync('which claude', { stdio: 'ignore' });
|
|
cliAvailable = true;
|
|
} catch {
|
|
cliAvailable = false;
|
|
}
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const fixturesDir = join(__dirname, '..', 'test-fixtures');
|
|
|
|
// ─── Test suite ──────────────────────────────────────────────────────────────
|
|
|
|
describe.skipIf(!cliAvailable)('E2E: Single plan execution', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-e2e-'));
|
|
// Copy fixture files to temp directory
|
|
await cp(fixturesDir, tmpDir, { recursive: true });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (tmpDir) {
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('executes a single plan and returns a valid PlanResult', async () => {
|
|
const gsd = new GSD({ projectDir: tmpDir, maxBudgetUsd: 1.0, maxTurns: 20 });
|
|
const result = await gsd.executePlan('sample-plan.md');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(typeof result.sessionId).toBe('string');
|
|
expect(result.sessionId.length).toBeGreaterThan(0);
|
|
expect(result.totalCostUsd).toBeGreaterThanOrEqual(0);
|
|
expect(result.durationMs).toBeGreaterThan(0);
|
|
expect(result.numTurns).toBeGreaterThan(0);
|
|
|
|
// Verify the plan's task was executed — output.txt should exist
|
|
const outputPath = join(tmpDir, 'output.txt');
|
|
const outputContent = await readFile(outputPath, 'utf-8');
|
|
expect(outputContent).toContain('hello from gsd-sdk');
|
|
}, 120_000); // 2 minute timeout for real CLI execution
|
|
|
|
it('proves session isolation (R014) — different session IDs for sequential runs', async () => {
|
|
// Create a second temp dir for isolation proof
|
|
const tmpDir2 = await mkdtemp(join(tmpdir(), 'gsd-sdk-e2e-'));
|
|
await cp(fixturesDir, tmpDir2, { recursive: true });
|
|
|
|
try {
|
|
const gsd1 = new GSD({ projectDir: tmpDir, maxBudgetUsd: 1.0, maxTurns: 20 });
|
|
const gsd2 = new GSD({ projectDir: tmpDir2, maxBudgetUsd: 1.0, maxTurns: 20 });
|
|
|
|
const result1 = await gsd1.executePlan('sample-plan.md');
|
|
const result2 = await gsd2.executePlan('sample-plan.md');
|
|
|
|
// Different sessions must have different session IDs
|
|
expect(result1.sessionId).not.toBe(result2.sessionId);
|
|
|
|
// Both should track cost independently
|
|
expect(result1.totalCostUsd).toBeGreaterThanOrEqual(0);
|
|
expect(result2.totalCostUsd).toBeGreaterThanOrEqual(0);
|
|
} finally {
|
|
await rm(tmpDir2, { recursive: true, force: true });
|
|
}
|
|
}, 240_000); // 4 minute timeout — two sequential runs
|
|
});
|
|
|
|
describe('E2E: Fixture validation (no CLI required)', () => {
|
|
it('fixture PLAN.md is valid and parseable', async () => {
|
|
const plan = await parsePlanFile(join(fixturesDir, 'sample-plan.md'));
|
|
|
|
expect(plan.frontmatter.phase).toBe('01-test');
|
|
expect(plan.frontmatter.plan).toBe('01');
|
|
expect(plan.frontmatter.type).toBe('execute');
|
|
expect(plan.frontmatter.wave).toBe(1);
|
|
expect(plan.frontmatter.depends_on).toEqual([]);
|
|
expect(plan.frontmatter.files_modified).toEqual(['output.txt']);
|
|
expect(plan.frontmatter.autonomous).toBe(true);
|
|
expect(plan.frontmatter.requirements).toEqual(['TEST-01']);
|
|
expect(plan.frontmatter.must_haves.truths).toEqual(['output.txt exists with expected content']);
|
|
|
|
expect(plan.objective).toContain('simple output file');
|
|
expect(plan.tasks).toHaveLength(1);
|
|
expect(plan.tasks[0].name).toBe('Create output file');
|
|
expect(plan.tasks[0].type).toBe('auto');
|
|
expect(plan.tasks[0].verify).toBe('test -f output.txt');
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!cliAvailable)('E2E: Event stream during plan execution (R007)', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-e2e-stream-'));
|
|
await cp(fixturesDir, tmpDir, { recursive: true });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (tmpDir) {
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('event stream emits events during plan execution (R007)', async () => {
|
|
const events: GSDEvent[] = [];
|
|
const gsd = new GSD({ projectDir: tmpDir, maxBudgetUsd: 1.0, maxTurns: 20 });
|
|
|
|
// Subscribe to all events
|
|
gsd.onEvent((event) => {
|
|
events.push(event);
|
|
});
|
|
|
|
const result = await gsd.executePlan('sample-plan.md');
|
|
expect(result.success).toBe(true);
|
|
|
|
// (a) At least one session_init event received
|
|
const initEvents = events.filter(e => e.type === GSDEventType.SessionInit);
|
|
expect(initEvents.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// (b) At least one tool_call event received
|
|
const toolCallEvents = events.filter(e => e.type === GSDEventType.ToolCall);
|
|
expect(toolCallEvents.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// (c) Exactly one session_complete event with cost >= 0
|
|
const completeEvents = events.filter(e => e.type === GSDEventType.SessionComplete);
|
|
expect(completeEvents).toHaveLength(1);
|
|
const completeEvent = completeEvents[0]!;
|
|
if (completeEvent.type === GSDEventType.SessionComplete) {
|
|
expect(completeEvent.totalCostUsd).toBeGreaterThanOrEqual(0);
|
|
}
|
|
|
|
// (d) Events arrived in order: session_init before tool_call before session_complete
|
|
const initIdx = events.findIndex(e => e.type === GSDEventType.SessionInit);
|
|
const toolCallIdx = events.findIndex(e => e.type === GSDEventType.ToolCall);
|
|
const completeIdx = events.findIndex(e => e.type === GSDEventType.SessionComplete);
|
|
expect(initIdx).toBeLessThan(toolCallIdx);
|
|
expect(toolCallIdx).toBeLessThan(completeIdx);
|
|
|
|
// Bonus: at least one cost_update event was emitted
|
|
const costEvents = events.filter(e => e.type === GSDEventType.CostUpdate);
|
|
expect(costEvents.length).toBeGreaterThanOrEqual(1);
|
|
}, 120_000);
|
|
});
|
|
|
|
describe('E2E: Error handling', () => {
|
|
it('returns failure for nonexistent plan path', async () => {
|
|
const tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-e2e-err-'));
|
|
|
|
try {
|
|
const gsd = new GSD({ projectDir: tmpDir });
|
|
await expect(gsd.executePlan('nonexistent-plan.md')).rejects.toThrow();
|
|
} finally {
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|