255 lines
8.7 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|