feat: Headless prompt overhaul — SDK drives full lifecycle without interactive patterns (#1419)
* fix: Created 10 headless prompt files (5 workflows + 5 agents) in sdk/p… - "sdk/prompts/workflows/execute-plan.md" - "sdk/prompts/workflows/research-phase.md" - "sdk/prompts/workflows/plan-phase.md" - "sdk/prompts/workflows/verify-phase.md" - "sdk/prompts/workflows/discuss-phase.md" - "sdk/prompts/agents/gsd-executor.md" - "sdk/prompts/agents/gsd-phase-researcher.md" - "sdk/prompts/agents/gsd-planner.md" GSD-Task: S01/T02 * feat: Created prompt-sanitizer.ts, wired headless prompt loading into P… - "sdk/src/prompt-sanitizer.ts" - "sdk/src/phase-prompt.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/gsd-tools.test.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S01/T01 * test: Added 111 unit tests covering sanitizePrompt(), headless prompt l… - "sdk/src/prompt-sanitizer.test.ts" - "sdk/src/headless-prompts.test.ts" - "sdk/src/phase-prompt.test.ts" GSD-Task: S01/T03 * feat: Wired sdkPromptsDir preference and sanitizePrompt into InitRunner… - "sdk/src/init-runner.ts" - "sdk/package.json" GSD-Task: S02/T01 * feat: add --init flag to auto command for single-command PRD-to-execution gsd-sdk auto --init @path/to/prd.md now bootstraps the project (init) then immediately runs the autonomous phase execution loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add remaining headless prompt files and templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: Extended init-runner.test.ts with 7 sdkPromptsDir preference and… - "sdk/src/init-runner.test.ts" GSD-Task: S02/T03 * test: Created sdk/src/assembled-prompts.test.ts with 74 tests verifying… - "sdk/src/assembled-prompts.test.ts" GSD-Task: S03/T01 * chore: auto-commit after complete-milestone GSD-Unit: M003-75c8bo --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
sdk/src/init-e2e.integration.test.ts
Normal file
135
sdk/src/init-e2e.integration.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* E2E integration test — proves InitRunner.run() drives real Agent SDK
|
||||
* sessions for the gsd-sdk init workflow.
|
||||
*
|
||||
* Requires Claude Code CLI (`claude`) installed and authenticated.
|
||||
* Skips gracefully if CLI is unavailable.
|
||||
*
|
||||
* This test proves the headless init pipeline can bootstrap a real project
|
||||
* without human intervention: setup → config → PROJECT.md → research →
|
||||
* synthesis → requirements → roadmap.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { mkdtemp, rm, readFile, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { InitRunner } from './init-runner.js';
|
||||
import { GSDTools } from './gsd-tools.js';
|
||||
import { GSDEventStream } from './event-stream.js';
|
||||
import { GSDEventType } from './types.js';
|
||||
import type { GSDEvent } from './types.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 sdkPromptsDir = join(__dirname, '..', 'prompts');
|
||||
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!cliAvailable)('E2E: InitRunner.run() full workflow', () => {
|
||||
let tmpDir: string;
|
||||
let events: GSDEvent[];
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-init-e2e-'));
|
||||
|
||||
// Initialize git in the temp dir (required by InitRunner)
|
||||
execSync('git init', { cwd: tmpDir, stdio: 'ignore' });
|
||||
execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'ignore' });
|
||||
execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'ignore' });
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (tmpDir) {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('InitRunner.run() bootstraps a project without human intervention', async () => {
|
||||
events = [];
|
||||
const eventStream = new GSDEventStream();
|
||||
eventStream.on('event', (e: GSDEvent) => events.push(e));
|
||||
|
||||
const tools = new GSDTools({
|
||||
projectDir: tmpDir,
|
||||
gsdToolsPath: GSD_TOOLS_PATH,
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
|
||||
const runner = new InitRunner({
|
||||
projectDir: tmpDir,
|
||||
tools,
|
||||
eventStream,
|
||||
config: {
|
||||
maxBudgetPerSession: 1.0,
|
||||
maxTurnsPerSession: 15,
|
||||
},
|
||||
sdkPromptsDir,
|
||||
});
|
||||
|
||||
const result = await runner.run('Build a CLI tool that prints hello world');
|
||||
|
||||
// ── Assert: pipeline executed (success OR at least 3+ steps completed) ──
|
||||
const completedSteps = result.steps.filter(s => s.success);
|
||||
const pipelineProgressed = result.success || completedSteps.length >= 3;
|
||||
expect(pipelineProgressed).toBe(true);
|
||||
|
||||
// ── Assert: config.json artifact created ──
|
||||
// config.json is written directly by InitRunner (not by Claude session)
|
||||
// so it should always exist if the config step succeeded
|
||||
const configStep = result.steps.find(s => s.step === 'config');
|
||||
if (configStep?.success) {
|
||||
const configPath = join(tmpDir, '.planning', 'config.json');
|
||||
const configStat = await stat(configPath).catch(() => null);
|
||||
expect(configStat).not.toBeNull();
|
||||
|
||||
if (configStat) {
|
||||
const configContent = JSON.parse(await readFile(configPath, 'utf-8'));
|
||||
expect(configContent.workflow.auto_advance).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Assert: PROJECT.md created if project step succeeded ──
|
||||
const projectStep = result.steps.find(s => s.step === 'project');
|
||||
if (projectStep?.success) {
|
||||
const projectPath = join(tmpDir, '.planning', 'PROJECT.md');
|
||||
const projectStat = await stat(projectPath).catch(() => null);
|
||||
expect(projectStat).not.toBeNull();
|
||||
}
|
||||
|
||||
// ── Assert: events captured include InitStart and at least one InitStepComplete ──
|
||||
const initStartEvents = events.filter(e => e.type === GSDEventType.InitStart);
|
||||
expect(initStartEvents.length).toBe(1);
|
||||
|
||||
const stepCompleteEvents = events.filter(e => e.type === GSDEventType.InitStepComplete);
|
||||
expect(stepCompleteEvents.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// ── Assert: InitComplete event emitted ──
|
||||
const initCompleteEvents = events.filter(e => e.type === GSDEventType.InitComplete);
|
||||
expect(initCompleteEvents.length).toBe(1);
|
||||
|
||||
// ── Assert: cost and duration are tracked ──
|
||||
expect(result.totalDurationMs).toBeGreaterThan(0);
|
||||
expect(typeof result.totalCostUsd).toBe('number');
|
||||
|
||||
// ── Assert: artifacts list is populated ──
|
||||
if (result.success) {
|
||||
expect(result.artifacts.length).toBeGreaterThan(0);
|
||||
expect(result.artifacts).toContain('.planning/config.json');
|
||||
}
|
||||
}, 600_000); // 10 minute timeout for the full 7-session init workflow
|
||||
});
|
||||
Reference in New Issue
Block a user