takt/src/__tests__/models.test.ts
nrslib d900ee8bc4 feat: answer status, autoCommit, permission_mode, verbose logging
- answer: planner が質問と判断したら COMPLETE で終了する仕組み
- autoCommit: worktree タスク完了時に自動 git commit
- permission_mode: workflow YAML でステップごとの権限指定
- verbose: verbose 時のファイル+stderr 二重出力修正
2026-01-28 10:02:04 +09:00

266 lines
8.1 KiB
TypeScript

/**
* Tests for takt models
*/
import { describe, it, expect } from 'vitest';
import {
AgentTypeSchema,
StatusSchema,
TransitionConditionSchema,
PermissionModeSchema,
WorkflowConfigRawSchema,
CustomAgentConfigSchema,
GlobalConfigSchema,
GENERIC_STATUS_PATTERNS,
} from '../models/schemas.js';
describe('AgentTypeSchema', () => {
it('should accept valid agent types', () => {
expect(AgentTypeSchema.parse('coder')).toBe('coder');
expect(AgentTypeSchema.parse('architect')).toBe('architect');
expect(AgentTypeSchema.parse('supervisor')).toBe('supervisor');
expect(AgentTypeSchema.parse('custom')).toBe('custom');
});
it('should reject invalid agent types', () => {
expect(() => AgentTypeSchema.parse('invalid')).toThrow();
});
});
describe('StatusSchema', () => {
it('should accept valid statuses', () => {
expect(StatusSchema.parse('pending')).toBe('pending');
expect(StatusSchema.parse('done')).toBe('done');
expect(StatusSchema.parse('approved')).toBe('approved');
expect(StatusSchema.parse('rejected')).toBe('rejected');
expect(StatusSchema.parse('blocked')).toBe('blocked');
expect(StatusSchema.parse('answer')).toBe('answer');
});
it('should reject invalid statuses', () => {
expect(() => StatusSchema.parse('unknown')).toThrow();
expect(() => StatusSchema.parse('conditional')).toThrow();
});
});
describe('TransitionConditionSchema', () => {
it('should accept valid conditions', () => {
expect(TransitionConditionSchema.parse('done')).toBe('done');
expect(TransitionConditionSchema.parse('approved')).toBe('approved');
expect(TransitionConditionSchema.parse('rejected')).toBe('rejected');
expect(TransitionConditionSchema.parse('always')).toBe('always');
expect(TransitionConditionSchema.parse('answer')).toBe('answer');
});
it('should reject invalid conditions', () => {
expect(() => TransitionConditionSchema.parse('conditional')).toThrow();
expect(() => TransitionConditionSchema.parse('fixed')).toThrow();
});
});
describe('PermissionModeSchema', () => {
it('should accept valid permission modes', () => {
expect(PermissionModeSchema.parse('default')).toBe('default');
expect(PermissionModeSchema.parse('acceptEdits')).toBe('acceptEdits');
expect(PermissionModeSchema.parse('bypassPermissions')).toBe('bypassPermissions');
});
it('should reject invalid permission modes', () => {
expect(() => PermissionModeSchema.parse('readOnly')).toThrow();
expect(() => PermissionModeSchema.parse('admin')).toThrow();
});
});
describe('WorkflowConfigRawSchema', () => {
it('should parse valid workflow config', () => {
const config = {
name: 'test-workflow',
description: 'A test workflow',
steps: [
{
name: 'step1',
agent: 'coder',
allowed_tools: ['Read', 'Grep'],
instruction: '{task}',
transitions: [
{ condition: 'done', next_step: 'COMPLETE' },
],
},
],
};
const result = WorkflowConfigRawSchema.parse(config);
expect(result.name).toBe('test-workflow');
expect(result.steps).toHaveLength(1);
expect(result.steps[0]?.allowed_tools).toEqual(['Read', 'Grep']);
expect(result.max_iterations).toBe(10);
});
it('should parse step with permission_mode', () => {
const config = {
name: 'test-workflow',
steps: [
{
name: 'implement',
agent: 'coder',
allowed_tools: ['Read', 'Edit', 'Write', 'Bash'],
permission_mode: 'acceptEdits',
instruction: '{task}',
transitions: [
{ condition: 'done', next_step: 'COMPLETE' },
],
},
],
};
const result = WorkflowConfigRawSchema.parse(config);
expect(result.steps[0]?.permission_mode).toBe('acceptEdits');
});
it('should allow omitting permission_mode', () => {
const config = {
name: 'test-workflow',
steps: [
{
name: 'plan',
agent: 'planner',
instruction: '{task}',
transitions: [],
},
],
};
const result = WorkflowConfigRawSchema.parse(config);
expect(result.steps[0]?.permission_mode).toBeUndefined();
});
it('should reject invalid permission_mode', () => {
const config = {
name: 'test-workflow',
steps: [
{
name: 'step1',
agent: 'coder',
permission_mode: 'superAdmin',
instruction: '{task}',
transitions: [],
},
],
};
expect(() => WorkflowConfigRawSchema.parse(config)).toThrow();
});
it('should require at least one step', () => {
const config = {
name: 'empty-workflow',
steps: [],
};
expect(() => WorkflowConfigRawSchema.parse(config)).toThrow();
});
});
describe('CustomAgentConfigSchema', () => {
it('should accept agent with prompt', () => {
const config = {
name: 'my-agent',
prompt: 'You are a helpful assistant.',
};
const result = CustomAgentConfigSchema.parse(config);
expect(result.name).toBe('my-agent');
});
it('should accept agent with prompt_file', () => {
const config = {
name: 'my-agent',
prompt_file: '/path/to/prompt.md',
};
const result = CustomAgentConfigSchema.parse(config);
expect(result.prompt_file).toBe('/path/to/prompt.md');
});
it('should accept agent with claude_agent', () => {
const config = {
name: 'my-agent',
claude_agent: 'architect',
};
const result = CustomAgentConfigSchema.parse(config);
expect(result.claude_agent).toBe('architect');
});
it('should accept agent with provider override', () => {
const config = {
name: 'my-agent',
prompt: 'You are a helpful assistant.',
provider: 'codex',
};
const result = CustomAgentConfigSchema.parse(config);
expect(result.provider).toBe('codex');
});
it('should reject agent without any prompt source', () => {
const config = {
name: 'my-agent',
};
expect(() => CustomAgentConfigSchema.parse(config)).toThrow();
});
});
describe('GlobalConfigSchema', () => {
it('should provide defaults', () => {
const config = {};
const result = GlobalConfigSchema.parse(config);
expect(result.trusted_directories).toEqual([]);
expect(result.default_workflow).toBe('default');
expect(result.log_level).toBe('info');
expect(result.provider).toBe('claude');
});
it('should accept valid config', () => {
const config = {
trusted_directories: ['/home/user/projects'],
default_workflow: 'custom',
log_level: 'debug' as const,
};
const result = GlobalConfigSchema.parse(config);
expect(result.trusted_directories).toHaveLength(1);
expect(result.log_level).toBe('debug');
});
});
describe('GENERIC_STATUS_PATTERNS', () => {
it('should have all standard status patterns', () => {
expect(GENERIC_STATUS_PATTERNS.approved).toBeDefined();
expect(GENERIC_STATUS_PATTERNS.rejected).toBeDefined();
expect(GENERIC_STATUS_PATTERNS.done).toBeDefined();
expect(GENERIC_STATUS_PATTERNS.blocked).toBeDefined();
expect(GENERIC_STATUS_PATTERNS.improve).toBeDefined();
expect(GENERIC_STATUS_PATTERNS.answer).toBeDefined();
});
it('should have valid regex patterns', () => {
for (const pattern of Object.values(GENERIC_STATUS_PATTERNS)) {
expect(() => new RegExp(pattern)).not.toThrow();
}
});
it('should match any [ROLE:COMMAND] format', () => {
// Generic patterns match any role
expect(new RegExp(GENERIC_STATUS_PATTERNS.approved).test('[CODER:APPROVE]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.approved).test('[MY_AGENT:APPROVE]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CUSTOM:DONE]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CODER:FIXED]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.improve).test('[MAGI:IMPROVE]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.answer).test('[PLANNER:ANSWER]')).toBe(true);
expect(new RegExp(GENERIC_STATUS_PATTERNS.answer).test('[MY_AGENT:ANSWER]')).toBe(true);
});
});