From 806fc04320aa78f3e650ba4f3d901949f0d96706 Mon Sep 17 00:00:00 2001 From: "hiraoku.shinichi" Date: Fri, 3 Jul 2026 08:54:55 +0900 Subject: [PATCH] Align builder payload parser with contract --- src/agents/builder.ts | 8 +++---- test/agent.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/agents/builder.ts b/src/agents/builder.ts index 4e60a1f..74feb49 100644 --- a/src/agents/builder.ts +++ b/src/agents/builder.ts @@ -8,8 +8,8 @@ import type { AgentAdapter, AgentRequest, AgentResult } from './types.js'; const builderPayloadSchema = z .object({ status: z.enum(['fixed', 'partial', 'blocked']), - summary: z.string().default(''), - notes: z.string().default(''), + summary: z.string(), + notes: z.string(), blockedReason: z.string().optional(), discoveredIssues: z .array( @@ -23,11 +23,11 @@ const builderPayloadSchema = z severity: z.string().optional(), labels: z.array(z.string()).optional() }) - .passthrough() + .strict() ) .default([]) }) - .passthrough(); + .strict(); export interface BuilderAgentOptions { command: string; diff --git a/test/agent.test.ts b/test/agent.test.ts index 201a8bc..da91cba 100644 --- a/test/agent.test.ts +++ b/test/agent.test.ts @@ -34,6 +34,24 @@ describe('parseAgentResult', () => { }); describe('BuilderAgentAdapter', () => { + async function runBuilderPayload(payload: Record) { + const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'kaizen-builder-')); + const runner: CommandRunner = async (command, args, options) => { + expect(command).toBe('builder-agent'); + expect(args).toEqual([]); + if (typeof options?.env?.KAIZEN_BUILD_RESULT_PATH !== 'string') throw new Error('missing result path'); + await fs.writeFile(options.env.KAIZEN_BUILD_RESULT_PATH, JSON.stringify(payload)); + return { command, args, cwd: options?.cwd, exitCode: 0, stdout: 'ok', stderr: '', durationMs: 123 }; + }; + + const adapter = new BuilderAgentAdapter(runner, { + command: 'builder-agent', + resultPath: '.kaizen/builder/build-result.json', + envAllowlist: ['PATH'] + }); + return adapter.run({ workspaceDir: workspace, prompt: 'fix it', timeoutMs: 1000 }); + } + it('reads the build result file produced by builder-agent', async () => { const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'kaizen-builder-')); const runner: CommandRunner = async (command, args, options) => { @@ -73,6 +91,40 @@ describe('BuilderAgentAdapter', () => { expect(result.discoveredIssues).toEqual([{ title: '別バグ', repo: 'kaizen-loop', body: '見つけた' }]); await expect(fs.access(path.join(workspace, '.kaizen', 'builder', 'build-result.json'))).rejects.toThrow(); }); + + it.each([ + ['summary', { status: 'fixed', notes: '' }], + ['notes', { status: 'fixed', summary: '直した' }] + ])('rejects builder payloads missing required %s', async (field, payload) => { + const result = await runBuilderPayload(payload); + + expect(result.status).toBe('error'); + expect(result.summary).toContain(field); + }); + + it('rejects unknown top-level builder payload fields', async () => { + const result = await runBuilderPayload({ + status: 'fixed', + summary: '直した', + notes: '', + extra: true + }); + + expect(result.status).toBe('error'); + expect(result.summary).toContain('extra'); + }); + + it('rejects unknown discovered issue fields', async () => { + const result = await runBuilderPayload({ + status: 'fixed', + summary: '直した', + notes: '', + discoveredIssues: [{ title: '別バグ', extra: true }] + }); + + expect(result.status).toBe('error'); + expect(result.summary).toContain('extra'); + }); }); describe('VerifierAgentAdapter', () => {