/** * Unit tests for `exceeded` status schema validation * * Covers: * - TaskRecordSchema cross-field validation for `exceeded` status * - TaskExecutionConfigSchema new fields: exceeded_max_movements, exceeded_current_iteration */ import { describe, it, expect } from 'vitest'; import { TaskRecordSchema, TaskExecutionConfigSchema, TaskStatusSchema, } from '../infra/task/schema.js'; function makeExceededRecord(overrides: Record = {}): Record { return { name: 'test-task', status: 'exceeded', 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', start_movement: 'plan', exceeded_max_movements: 60, exceeded_current_iteration: 30, ...overrides, }; } describe('TaskStatusSchema', () => { it('should accept exceeded as a valid status', () => { expect(() => TaskStatusSchema.parse('exceeded')).not.toThrow(); }); it('should still accept all existing statuses', () => { expect(() => TaskStatusSchema.parse('pending')).not.toThrow(); expect(() => TaskStatusSchema.parse('running')).not.toThrow(); expect(() => TaskStatusSchema.parse('completed')).not.toThrow(); expect(() => TaskStatusSchema.parse('failed')).not.toThrow(); }); it('should reject unknown status', () => { expect(() => TaskStatusSchema.parse('unknown')).toThrow(); }); }); describe('TaskExecutionConfigSchema - exceeded fields', () => { it('should accept exceeded_max_movements as a positive integer', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_max_movements: 60 })).not.toThrow(); }); it('should accept exceeded_current_iteration as a non-negative integer', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_current_iteration: 30 })).not.toThrow(); }); it('should accept exceeded_current_iteration as zero', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_current_iteration: 0 })).not.toThrow(); }); it('should accept both fields together', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_max_movements: 60, exceeded_current_iteration: 30, })).not.toThrow(); }); it('should accept config without exceeded fields (optional)', () => { expect(() => TaskExecutionConfigSchema.parse({})).not.toThrow(); }); it('should reject exceeded_max_movements as zero', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_max_movements: 0 })).toThrow(); }); it('should reject exceeded_max_movements as negative', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_max_movements: -1 })).toThrow(); }); it('should reject exceeded_max_movements as non-integer', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_max_movements: 1.5 })).toThrow(); }); it('should reject exceeded_current_iteration as negative', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_current_iteration: -1 })).toThrow(); }); it('should reject exceeded_current_iteration as non-integer', () => { expect(() => TaskExecutionConfigSchema.parse({ exceeded_current_iteration: 0.5 })).toThrow(); }); }); describe('TaskRecordSchema - exceeded status', () => { describe('valid exceeded record', () => { it('should accept a valid exceeded record with all required fields', () => { expect(() => TaskRecordSchema.parse(makeExceededRecord())).not.toThrow(); }); it('should accept exceeded record without start_movement (optional)', () => { const record = makeExceededRecord({ start_movement: undefined }); expect(() => TaskRecordSchema.parse(record)).not.toThrow(); }); it('should reject exceeded record with only exceeded_current_iteration set (exceeded_max_movements missing)', () => { const record = makeExceededRecord({ exceeded_max_movements: undefined }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); it('should reject exceeded record with only exceeded_max_movements set (exceeded_current_iteration missing)', () => { const record = makeExceededRecord({ exceeded_current_iteration: undefined }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); it('should accept exceeded record when both exceeded fields are absent (neither field set)', () => { const record = makeExceededRecord({ exceeded_max_movements: undefined, exceeded_current_iteration: undefined }); expect(() => TaskRecordSchema.parse(record)).not.toThrow(); }); }); describe('started_at requirement', () => { it('should reject exceeded record without started_at (null)', () => { const record = makeExceededRecord({ started_at: null }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); }); describe('completed_at requirement', () => { it('should reject exceeded record without completed_at (null)', () => { const record = makeExceededRecord({ completed_at: null }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); }); describe('failure prohibition', () => { it('should reject exceeded record with failure field', () => { const record = makeExceededRecord({ failure: { error: 'something' } }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); }); describe('owner_pid prohibition', () => { it('should reject exceeded record with owner_pid set to a process ID', () => { const record = makeExceededRecord({ owner_pid: 12345 }); expect(() => TaskRecordSchema.parse(record)).toThrow(); }); it('should accept exceeded record with owner_pid explicitly null', () => { const record = makeExceededRecord({ owner_pid: null }); expect(() => TaskRecordSchema.parse(record)).not.toThrow(); }); }); describe('independence from other statuses', () => { it('should not affect pending status validation', () => { // pending: started_at must be null expect(() => TaskRecordSchema.parse({ name: 'test-task', status: 'pending', content: 'task content', created_at: '2025-01-01T00:00:00.000Z', started_at: null, completed_at: null, })).not.toThrow(); }); it('should not affect failed status validation', () => { // failed: requires failure field expect(() => TaskRecordSchema.parse({ name: 'test-task', status: 'failed', 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' }, })).not.toThrow(); }); }); });