takt/src/__tests__/task-schema.test.ts

255 lines
8.7 KiB
TypeScript

/**
* Unit tests for task schema validation
*
* Tests TaskRecordSchema cross-field validation rules (status-dependent constraints).
*/
import { describe, it, expect } from 'vitest';
import {
TaskRecordSchema,
TaskFileSchema,
TaskExecutionConfigSchema,
} from '../infra/task/schema.js';
function makePendingRecord() {
return {
name: 'test-task',
status: 'pending' as const,
content: 'task content',
created_at: '2025-01-01T00:00:00.000Z',
started_at: null,
completed_at: null,
};
}
function makeRunningRecord() {
return {
name: 'test-task',
status: 'running' as const,
content: 'task content',
created_at: '2025-01-01T00:00:00.000Z',
started_at: '2025-01-01T01:00:00.000Z',
completed_at: null,
};
}
function makeCompletedRecord() {
return {
name: 'test-task',
status: 'completed' as const,
content: 'task content',
created_at: '2025-01-01T00:00:00.000Z',
started_at: '2025-01-01T01:00:00.000Z',
completed_at: '2025-01-01T02:00:00.000Z',
};
}
function makeFailedRecord() {
return {
name: 'test-task',
status: 'failed' as const,
content: 'task content',
created_at: '2025-01-01T00:00:00.000Z',
started_at: '2025-01-01T01:00:00.000Z',
completed_at: '2025-01-01T02:00:00.000Z',
failure: { error: 'something went wrong' },
};
}
describe('TaskExecutionConfigSchema', () => {
it('should accept valid config with all optional fields', () => {
const config = {
worktree: true,
branch: 'feature/test',
piece: 'unit-test',
issue: 42,
start_movement: 'plan',
retry_note: 'retry after fix',
auto_pr: true,
};
expect(() => TaskExecutionConfigSchema.parse(config)).not.toThrow();
});
it('should accept empty config (all fields optional)', () => {
expect(() => TaskExecutionConfigSchema.parse({})).not.toThrow();
});
it('should accept worktree as string', () => {
expect(() => TaskExecutionConfigSchema.parse({ worktree: '/custom/path' })).not.toThrow();
});
it('should reject negative issue number', () => {
expect(() => TaskExecutionConfigSchema.parse({ issue: -1 })).toThrow();
});
it('should reject non-integer issue number', () => {
expect(() => TaskExecutionConfigSchema.parse({ issue: 1.5 })).toThrow();
});
});
describe('TaskFileSchema', () => {
it('should accept valid task with required fields', () => {
expect(() => TaskFileSchema.parse({ task: 'do something' })).not.toThrow();
});
it('should reject empty task string', () => {
expect(() => TaskFileSchema.parse({ task: '' })).toThrow();
});
it('should reject missing task field', () => {
expect(() => TaskFileSchema.parse({})).toThrow();
});
});
describe('TaskRecordSchema', () => {
describe('pending status', () => {
it('should accept valid pending record', () => {
expect(() => TaskRecordSchema.parse(makePendingRecord())).not.toThrow();
});
it('should reject pending record with started_at', () => {
const record = { ...makePendingRecord(), started_at: '2025-01-01T01:00:00.000Z' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject pending record with completed_at', () => {
const record = { ...makePendingRecord(), completed_at: '2025-01-01T02:00:00.000Z' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject pending record with failure', () => {
const record = { ...makePendingRecord(), failure: { error: 'fail' } };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject pending record with owner_pid', () => {
const record = { ...makePendingRecord(), owner_pid: 1234 };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
});
describe('running status', () => {
it('should accept valid running record', () => {
expect(() => TaskRecordSchema.parse(makeRunningRecord())).not.toThrow();
});
it('should reject running record without started_at', () => {
const record = { ...makeRunningRecord(), started_at: null };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject running record with completed_at', () => {
const record = { ...makeRunningRecord(), completed_at: '2025-01-01T02:00:00.000Z' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject running record with failure', () => {
const record = { ...makeRunningRecord(), failure: { error: 'fail' } };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should accept running record with owner_pid', () => {
const record = { ...makeRunningRecord(), owner_pid: 5678 };
expect(() => TaskRecordSchema.parse(record)).not.toThrow();
});
});
describe('completed status', () => {
it('should accept valid completed record', () => {
expect(() => TaskRecordSchema.parse(makeCompletedRecord())).not.toThrow();
});
it('should reject completed record without started_at', () => {
const record = { ...makeCompletedRecord(), started_at: null };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject completed record without completed_at', () => {
const record = { ...makeCompletedRecord(), completed_at: null };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject completed record with failure', () => {
const record = { ...makeCompletedRecord(), failure: { error: 'fail' } };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject completed record with owner_pid', () => {
const record = { ...makeCompletedRecord(), owner_pid: 1234 };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
});
describe('failed status', () => {
it('should accept valid failed record', () => {
expect(() => TaskRecordSchema.parse(makeFailedRecord())).not.toThrow();
});
it('should reject failed record without started_at', () => {
const record = { ...makeFailedRecord(), started_at: null };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject failed record without completed_at', () => {
const record = { ...makeFailedRecord(), completed_at: null };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject failed record without failure', () => {
const record = { ...makeFailedRecord(), failure: undefined };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject failed record with owner_pid', () => {
const record = { ...makeFailedRecord(), owner_pid: 1234 };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
});
describe('content requirement', () => {
it('should accept record with content', () => {
expect(() => TaskRecordSchema.parse(makePendingRecord())).not.toThrow();
});
it('should accept record with content_file', () => {
const record = { ...makePendingRecord(), content: undefined, content_file: './task.md' };
expect(() => TaskRecordSchema.parse(record)).not.toThrow();
});
it('should accept record with task_dir', () => {
const record = { ...makePendingRecord(), content: undefined, task_dir: '.takt/tasks/20260201-000000-task' };
expect(() => TaskRecordSchema.parse(record)).not.toThrow();
});
it('should reject record with neither content, content_file, nor task_dir', () => {
const record = { ...makePendingRecord(), content: undefined };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject record with both content and task_dir', () => {
const record = { ...makePendingRecord(), task_dir: '.takt/tasks/20260201-000000-task' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject record with invalid task_dir format', () => {
const record = { ...makePendingRecord(), content: undefined, task_dir: '.takt/reports/invalid' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject record with parent-directory task_dir', () => {
const record = { ...makePendingRecord(), content: undefined, task_dir: '.takt/tasks/..' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject record with empty content', () => {
const record = { ...makePendingRecord(), content: '' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
it('should reject record with empty content_file', () => {
const record = { ...makePendingRecord(), content: undefined, content_file: '' };
expect(() => TaskRecordSchema.parse(record)).toThrow();
});
});
});