takt/src/__tests__/it-instruction-builder.test.ts
2026-02-02 13:06:12 +09:00

371 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Instruction builder integration tests.
*
* Tests template variable expansion and auto-injection in buildInstruction().
* Uses real workflow step configs (not mocked) against the buildInstruction function.
*
* Not mocked: buildInstruction, buildReportInstruction, buildStatusJudgmentInstruction
*/
import { describe, it, expect, vi } from 'vitest';
import type { WorkflowStep, WorkflowRule, AgentResponse } from '../models/types.js';
vi.mock('../config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'),
}));
import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js';
import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js';
import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js';
import type { InstructionContext } from '../workflow/instruction/instruction-context.js';
// Function wrappers for test readability
function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string {
return new InstructionBuilder(step, ctx).build();
}
function buildReportInstruction(step: WorkflowStep, ctx: ReportInstructionContext): string {
return new ReportInstructionBuilder(step, ctx).build();
}
function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string {
return new StatusJudgmentBuilder(step, ctx).build();
}
// --- Test helpers ---
function makeRule(condition: string, next: string, extra?: Partial<WorkflowRule>): WorkflowRule {
return { condition, next, ...extra };
}
function makeStep(overrides: Partial<WorkflowStep> = {}): WorkflowStep {
return {
name: 'test-step',
agent: 'test-agent',
agentDisplayName: 'test-step',
instructionTemplate: 'Do the work.',
passPreviousResponse: false,
rules: [
makeRule('Done', 'COMPLETE'),
makeRule('Not done', 'ABORT'),
],
...overrides,
};
}
function makeContext(overrides: Partial<InstructionContext> = {}): InstructionContext {
return {
task: 'Test task description',
iteration: 3,
maxIterations: 30,
stepIteration: 2,
cwd: '/tmp/test-project',
projectCwd: '/tmp/test-project',
userInputs: [],
language: 'en',
...overrides,
};
}
describe('Instruction Builder IT: task auto-injection', () => {
it('should auto-inject task as "User Request" section when template has no {task}', () => {
const step = makeStep({ instructionTemplate: 'Do the work.' });
const ctx = makeContext({ task: 'Build the login page' });
const result = buildInstruction(step, ctx);
expect(result).toContain('## User Request');
expect(result).toContain('Build the login page');
});
it('should NOT auto-inject task section when template contains {task}', () => {
const step = makeStep({ instructionTemplate: 'Here is the task: {task}' });
const ctx = makeContext({ task: 'Build the login page' });
const result = buildInstruction(step, ctx);
// Should not have separate User Request section
const userRequestCount = (result.match(/## User Request/g) || []).length;
expect(userRequestCount).toBe(0);
// But task should still appear inline
expect(result).toContain('Build the login page');
});
});
describe('Instruction Builder IT: previous_response auto-injection', () => {
it('should auto-inject previous response when passPreviousResponse is true', () => {
const step = makeStep({
passPreviousResponse: true,
instructionTemplate: 'Continue the work.',
});
const previousOutput: AgentResponse = {
agent: 'previous-agent',
status: 'done',
content: 'Previous agent completed step A.',
timestamp: new Date(),
};
const ctx = makeContext({ previousOutput });
const result = buildInstruction(step, ctx);
expect(result).toContain('## Previous Response');
expect(result).toContain('Previous agent completed step A.');
});
it('should NOT inject previous response when passPreviousResponse is false', () => {
const step = makeStep({
passPreviousResponse: false,
instructionTemplate: 'Do fresh work.',
});
const previousOutput: AgentResponse = {
agent: 'previous-agent',
status: 'done',
content: 'Previous output.',
timestamp: new Date(),
};
const ctx = makeContext({ previousOutput });
const result = buildInstruction(step, ctx);
expect(result).not.toContain('## Previous Response');
expect(result).not.toContain('Previous output.');
});
it('should NOT auto-inject when template contains {previous_response}', () => {
const step = makeStep({
passPreviousResponse: true,
instructionTemplate: '## Context\n{previous_response}\n\nDo work.',
});
const previousOutput: AgentResponse = {
agent: 'prev', status: 'done', content: 'Prior work done.', timestamp: new Date(),
};
const ctx = makeContext({ previousOutput });
const result = buildInstruction(step, ctx);
// Should not have separate Previous Response section
const prevCount = (result.match(/## Previous Response/g) || []).length;
expect(prevCount).toBe(0);
// But content should be inline
expect(result).toContain('Prior work done.');
});
});
describe('Instruction Builder IT: user_inputs auto-injection', () => {
it('should auto-inject user inputs section', () => {
const step = makeStep();
const ctx = makeContext({ userInputs: ['Fix the typo', 'Use TypeScript'] });
const result = buildInstruction(step, ctx);
expect(result).toContain('## Additional User Inputs');
expect(result).toContain('Fix the typo');
expect(result).toContain('Use TypeScript');
});
it('should NOT auto-inject when template contains {user_inputs}', () => {
const step = makeStep({ instructionTemplate: 'Inputs: {user_inputs}' });
const ctx = makeContext({ userInputs: ['Input A'] });
const result = buildInstruction(step, ctx);
const count = (result.match(/## Additional User Inputs/g) || []).length;
expect(count).toBe(0);
expect(result).toContain('Input A');
});
});
describe('Instruction Builder IT: iteration variables', () => {
it('should replace {iteration}, {max_iterations}, {step_iteration} in template', () => {
const step = makeStep({
instructionTemplate: 'Iter: {iteration}/{max_iterations}, step iter: {step_iteration}',
});
const ctx = makeContext({ iteration: 5, maxIterations: 30, stepIteration: 2 });
const result = buildInstruction(step, ctx);
expect(result).toContain('Iter: 5/30, step iter: 2');
});
it('should include iteration in Workflow Context section', () => {
const step = makeStep();
const ctx = makeContext({ iteration: 7, maxIterations: 20, stepIteration: 3 });
const result = buildInstruction(step, ctx);
expect(result).toContain('Iteration: 7/20');
expect(result).toContain('Step Iteration: 3');
});
});
describe('Instruction Builder IT: report_dir expansion', () => {
it('should replace {report_dir} in template', () => {
const step = makeStep({
instructionTemplate: 'Read the plan from {report_dir}/00-plan.md',
});
const ctx = makeContext({ reportDir: '/tmp/test-project/.takt/reports/20250126-task' });
const result = buildInstruction(step, ctx);
expect(result).toContain('Read the plan from /tmp/test-project/.takt/reports/20250126-task/00-plan.md');
});
it('should replace {report:filename} with full path', () => {
const step = makeStep({
instructionTemplate: 'Read {report:00-plan.md} for the plan.',
});
const ctx = makeContext({ reportDir: '/tmp/reports' });
const result = buildInstruction(step, ctx);
expect(result).toContain('Read /tmp/reports/00-plan.md for the plan.');
});
});
describe('Instruction Builder IT: status output rules injection', () => {
it('should inject status rules for steps with tag-based rules', () => {
const step = makeStep({
name: 'plan',
rules: [
makeRule('Requirements clear', 'implement'),
makeRule('Requirements unclear', 'ABORT'),
],
});
const ctx = makeContext();
const result = buildInstruction(step, ctx);
// Should contain status rules section with the tag format
expect(result).toContain('[PLAN:');
expect(result).toContain('Requirements clear');
expect(result).toContain('Requirements unclear');
});
it('should NOT inject status rules for steps with only ai() conditions', () => {
const step = makeStep({
name: 'review',
rules: [
makeRule('ai("approved")', 'COMPLETE', { isAiCondition: true, aiConditionText: 'approved' }),
makeRule('ai("rejected")', 'ABORT', { isAiCondition: true, aiConditionText: 'rejected' }),
],
});
const ctx = makeContext();
const result = buildInstruction(step, ctx);
// Should NOT contain tag-based status rules
expect(result).not.toContain('[REVIEW:');
});
});
describe('Instruction Builder IT: edit permission in execution context', () => {
it('should include edit permission rules when edit is true', () => {
const step = makeStep({ edit: true });
const ctx = makeContext();
const result = buildInstruction(step, ctx);
// Should mention editing is allowed
expect(result).toMatch(/edit|Edit|ファイル/i);
});
it('should indicate read-only when edit is false', () => {
const step = makeStep({ edit: false });
const ctx = makeContext();
const result = buildInstruction(step, ctx);
// Should contain the "Editing is DISABLED" execution rule
expect(result).toContain('Editing is DISABLED');
expect(result).not.toContain('Editing is ENABLED');
});
});
describe('Instruction Builder IT: buildReportInstruction', () => {
it('should build report instruction with report context', () => {
const step = makeStep({
name: 'plan',
report: { name: '00-plan.md', format: '# Plan\n{step_iteration}' },
});
const result = buildReportInstruction(step, {
cwd: '/tmp/test',
reportDir: '/tmp/test/.takt/reports/test-dir',
stepIteration: 1,
language: 'en',
});
expect(result).toContain('00-plan.md');
expect(result).toContain('/tmp/test/.takt/reports/test-dir');
expect(result).toContain('report');
});
it('should throw for step without report config', () => {
const step = makeStep({ report: undefined });
expect(() =>
buildReportInstruction(step, {
cwd: '/tmp',
reportDir: '/tmp/reports',
stepIteration: 1,
}),
).toThrow(/no report config/);
});
});
describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => {
it('should build Phase 3 instruction with status rules', () => {
const step = makeStep({
name: 'plan',
rules: [
makeRule('Clear', 'implement'),
makeRule('Unclear', 'ABORT'),
],
});
const result = buildStatusJudgmentInstruction(step, { language: 'en' });
expect(result).toContain('[PLAN:');
expect(result).toContain('Clear');
expect(result).toContain('Unclear');
});
it('should throw for step without rules', () => {
const step = makeStep({ rules: undefined });
expect(() =>
buildStatusJudgmentInstruction(step, { language: 'en' }),
).toThrow(/no rules/);
});
});
describe('Instruction Builder IT: template injection prevention', () => {
it('should escape curly braces in task content', () => {
const step = makeStep();
const ctx = makeContext({ task: 'Use {variable} in code' });
const result = buildInstruction(step, ctx);
// Braces should be escaped to prevent template re-injection
expect(result).not.toContain('{variable}');
expect(result).toContain('variable');
});
it('should escape curly braces in previous response content', () => {
const step = makeStep({
passPreviousResponse: true,
instructionTemplate: 'Continue.',
});
const ctx = makeContext({
previousOutput: {
agent: 'prev', status: 'done',
content: 'Use {template} syntax', timestamp: new Date(),
},
});
const result = buildInstruction(step, ctx);
expect(result).not.toContain('{template}');
expect(result).toContain('template');
});
});