takt/src/__tests__/arpeggio-schema.test.ts
nrs 7e15691ba2
github-issue-200-arpeggio (#203)
* fix: stable release時にnext dist-tagを自動同期

* takt: github-issue-200-arpeggio
2026-02-10 13:37:15 +09:00

333 lines
9.2 KiB
TypeScript

/**
* Tests for Arpeggio-related Zod schemas.
*
* Covers:
* - ArpeggioMergeRawSchema cross-validation (.refine())
* - ArpeggioConfigRawSchema required fields and defaults
* - PieceMovementRawSchema with arpeggio field
*/
import { describe, it, expect } from 'vitest';
import {
ArpeggioMergeRawSchema,
ArpeggioConfigRawSchema,
PieceMovementRawSchema,
} from '../core/models/index.js';
describe('ArpeggioMergeRawSchema', () => {
it('should accept concat strategy without inline_js or file', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'concat',
});
expect(result.success).toBe(true);
});
it('should accept concat strategy with separator', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'concat',
separator: '\n---\n',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.separator).toBe('\n---\n');
}
});
it('should default strategy to concat when omitted', () => {
const result = ArpeggioMergeRawSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.strategy).toBe('concat');
}
});
it('should accept custom strategy with inline_js', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'custom',
inline_js: 'return results.map(r => r.content).join(",");',
});
expect(result.success).toBe(true);
});
it('should accept custom strategy with file', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'custom',
file: './merge.js',
});
expect(result.success).toBe(true);
});
it('should reject custom strategy without inline_js or file', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'custom',
});
expect(result.success).toBe(false);
});
it('should reject concat strategy with inline_js', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'concat',
inline_js: 'return "hello";',
});
expect(result.success).toBe(false);
});
it('should reject concat strategy with file', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'concat',
file: './merge.js',
});
expect(result.success).toBe(false);
});
it('should reject invalid strategy value', () => {
const result = ArpeggioMergeRawSchema.safeParse({
strategy: 'invalid',
});
expect(result.success).toBe(false);
});
});
describe('ArpeggioConfigRawSchema', () => {
const validConfig = {
source: 'csv',
source_path: './data.csv',
template: './template.md',
};
it('should accept a valid minimal config', () => {
const result = ArpeggioConfigRawSchema.safeParse(validConfig);
expect(result.success).toBe(true);
});
it('should apply default values for optional fields', () => {
const result = ArpeggioConfigRawSchema.safeParse(validConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.batch_size).toBe(1);
expect(result.data.concurrency).toBe(1);
expect(result.data.max_retries).toBe(2);
expect(result.data.retry_delay_ms).toBe(1000);
}
});
it('should accept explicit values overriding defaults', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
batch_size: 5,
concurrency: 3,
max_retries: 4,
retry_delay_ms: 2000,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.batch_size).toBe(5);
expect(result.data.concurrency).toBe(3);
expect(result.data.max_retries).toBe(4);
expect(result.data.retry_delay_ms).toBe(2000);
}
});
it('should accept config with merge field', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
merge: { strategy: 'concat', separator: '---' },
});
expect(result.success).toBe(true);
});
it('should accept config with output_path', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
output_path: './output.txt',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.output_path).toBe('./output.txt');
}
});
it('should reject when source is empty', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
source: '',
});
expect(result.success).toBe(false);
});
it('should reject when source is missing', () => {
const { source: _, ...noSource } = validConfig;
const result = ArpeggioConfigRawSchema.safeParse(noSource);
expect(result.success).toBe(false);
});
it('should reject when source_path is empty', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
source_path: '',
});
expect(result.success).toBe(false);
});
it('should reject when source_path is missing', () => {
const { source_path: _, ...noSourcePath } = validConfig;
const result = ArpeggioConfigRawSchema.safeParse(noSourcePath);
expect(result.success).toBe(false);
});
it('should reject when template is empty', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
template: '',
});
expect(result.success).toBe(false);
});
it('should reject when template is missing', () => {
const { template: _, ...noTemplate } = validConfig;
const result = ArpeggioConfigRawSchema.safeParse(noTemplate);
expect(result.success).toBe(false);
});
it('should reject batch_size of 0', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
batch_size: 0,
});
expect(result.success).toBe(false);
});
it('should reject negative batch_size', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
batch_size: -1,
});
expect(result.success).toBe(false);
});
it('should reject concurrency of 0', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
concurrency: 0,
});
expect(result.success).toBe(false);
});
it('should reject negative concurrency', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
concurrency: -1,
});
expect(result.success).toBe(false);
});
it('should reject negative max_retries', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
max_retries: -1,
});
expect(result.success).toBe(false);
});
it('should accept max_retries of 0', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
max_retries: 0,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.max_retries).toBe(0);
}
});
it('should reject non-integer batch_size', () => {
const result = ArpeggioConfigRawSchema.safeParse({
...validConfig,
batch_size: 1.5,
});
expect(result.success).toBe(false);
});
});
describe('PieceMovementRawSchema with arpeggio', () => {
it('should accept a movement with arpeggio config', () => {
const raw = {
name: 'batch-process',
arpeggio: {
source: 'csv',
source_path: './data.csv',
template: './prompt.md',
},
};
const result = PieceMovementRawSchema.safeParse(raw);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.arpeggio).toBeDefined();
expect(result.data.arpeggio!.source).toBe('csv');
}
});
it('should accept a movement with arpeggio and rules', () => {
const raw = {
name: 'batch-process',
arpeggio: {
source: 'csv',
source_path: './data.csv',
template: './prompt.md',
batch_size: 2,
concurrency: 3,
},
rules: [
{ condition: 'All processed', next: 'COMPLETE' },
{ condition: 'Errors found', next: 'fix' },
],
};
const result = PieceMovementRawSchema.safeParse(raw);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.arpeggio!.batch_size).toBe(2);
expect(result.data.arpeggio!.concurrency).toBe(3);
expect(result.data.rules).toHaveLength(2);
}
});
it('should accept a movement without arpeggio (normal movement)', () => {
const raw = {
name: 'normal-step',
persona: 'coder.md',
instruction_template: 'Do work',
};
const result = PieceMovementRawSchema.safeParse(raw);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.arpeggio).toBeUndefined();
}
});
it('should accept a movement with arpeggio including custom merge', () => {
const raw = {
name: 'custom-merge-step',
arpeggio: {
source: 'csv',
source_path: './data.csv',
template: './prompt.md',
merge: {
strategy: 'custom',
inline_js: 'return results.map(r => r.content).join(", ");',
},
output_path: './output.txt',
},
};
const result = PieceMovementRawSchema.safeParse(raw);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.arpeggio!.merge).toBeDefined();
expect(result.data.arpeggio!.output_path).toBe('./output.txt');
}
});
});