import { describe, it, expect } from 'vitest'; import { dagNodeSchema, promptNodeSchema, commandNodeSchema, bashNodeSchema, scriptNodeSchema, loopNodeSchema, approvalNodeSchema, cancelNodeSchema, loopNodeConfigSchema, stepRetryConfigSchema, triggerRuleSchema, isBashNode, isLoopNode, isApprovalNode, isCancelNode, isScriptNode, isPromptNode, isCommandNode, effortLevelSchema, thinkingConfigSchema, approvalOnRejectSchema, dagNodeBaseSchema, } from '../index.js'; import type { DagNode, PromptNode, CommandNode, BashNode, ScriptNode, LoopNode, ApprovalNode, CancelNode, } from '../index.js'; const validBase = { id: 'test-node', depends_on: [], }; describe('dagNodeSchema', () => { it('validates a prompt node', () => { const node = { ...validBase, kind: 'prompt' as const, prompt: 'Do the thing' }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates a command node', () => { const node = { ...validBase, kind: 'command' as const, command: 'echo hello' }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates a bash node', () => { const node = { ...validBase, kind: 'bash' as const, bash: 'echo hello' }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates a script node', () => { const node = { ...validBase, kind: 'script' as const, script: 'print(1)', runtime: 'bun' as const }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates a loop node', () => { const node = { ...validBase, kind: 'loop' as const, config: { prompt: 'iterate', until: 'done', max_iterations: 5 }, }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates an approval node', () => { const node = { ...validBase, kind: 'approval' as const, message: 'Approve this?' }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('validates a cancel node', () => { const node = { ...validBase, kind: 'cancel' as const }; const result = dagNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it('rejects a node with no kind field', () => { const result = dagNodeSchema.safeParse({ id: 'x', prompt: 'y' }); expect(result.success).toBe(false); }); it('rejects a node with an invalid kind', () => { const result = dagNodeSchema.safeParse({ ...validBase, kind: 'unknown' }); expect(result.success).toBe(false); }); }); describe('node type unique fields', () => { it('prompt node requires prompt or command_file', () => { const result = promptNodeSchema.safeParse({ ...validBase, kind: 'prompt' }); expect(result.success).toBe(true); }); it('command node requires command', () => { const result = commandNodeSchema.safeParse({ ...validBase, kind: 'command' }); expect(result.success).toBe(false); }); it('bash node requires bash', () => { const result = bashNodeSchema.safeParse({ ...validBase, kind: 'bash' }); expect(result.success).toBe(false); }); it('script node requires script and runtime', () => { const noScript = scriptNodeSchema.safeParse({ ...validBase, kind: 'script', runtime: 'bun' }); expect(noScript.success).toBe(false); const noRuntime = scriptNodeSchema.safeParse({ ...validBase, kind: 'script', script: 'print(1)' }); expect(noRuntime.success).toBe(false); const valid = scriptNodeSchema.safeParse({ ...validBase, kind: 'script', script: 'print(1)', runtime: 'bun', }); expect(valid.success).toBe(true); }); it('loop node requires config', () => { const result = loopNodeSchema.safeParse({ ...validBase, kind: 'loop' }); expect(result.success).toBe(false); }); it('approval node requires message', () => { const result = approvalNodeSchema.safeParse({ ...validBase, kind: 'approval' }); expect(result.success).toBe(false); }); it('cancel node does not require reason', () => { const result = cancelNodeSchema.safeParse({ ...validBase, kind: 'cancel' }); expect(result.success).toBe(true); }); }); describe('loopNodeConfigSchema', () => { it('validates a minimal config', () => { const result = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: 5, }); expect(result.success).toBe(true); }); it('rejects empty prompt', () => { const result = loopNodeConfigSchema.safeParse({ prompt: '', until: 'done', max_iterations: 5, }); expect(result.success).toBe(false); }); it('rejects empty until', () => { const result = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: '', max_iterations: 5, }); expect(result.success).toBe(false); }); it('rejects non-positive max_iterations', () => { const zeroResult = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: 0, }); expect(zeroResult.success).toBe(false); const negResult = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: -1, }); expect(negResult.success).toBe(false); }); it('requires gate_message when interactive is true', () => { const noGate = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: 5, interactive: true, }); expect(noGate.success).toBe(false); const withGate = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: 5, interactive: true, gate_message: 'Approve this iteration?', }); expect(withGate.success).toBe(true); }); it('allows interactive=false without gate_message', () => { const result = loopNodeConfigSchema.safeParse({ prompt: 'iterate', until: 'done', max_iterations: 5, interactive: false, }); expect(result.success).toBe(true); }); }); describe('stepRetryConfigSchema', () => { it('validates a minimal retry config', () => { const result = stepRetryConfigSchema.safeParse({ max_attempts: 3 }); expect(result.success).toBe(true); }); it('rejects max_attempts below 1', () => { const result = stepRetryConfigSchema.safeParse({ max_attempts: 0 }); expect(result.success).toBe(false); }); it('rejects max_attempts above 5', () => { const result = stepRetryConfigSchema.safeParse({ max_attempts: 6 }); expect(result.success).toBe(false); }); it('accepts max_attempts at boundaries (1 and 5)', () => { expect(stepRetryConfigSchema.safeParse({ max_attempts: 1 }).success).toBe(true); expect(stepRetryConfigSchema.safeParse({ max_attempts: 5 }).success).toBe(true); }); it('rejects delay_ms below 1000', () => { const result = stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 500 }); expect(result.success).toBe(false); }); it('rejects delay_ms above 60000', () => { const result = stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 70000 }); expect(result.success).toBe(false); }); it('accepts delay_ms at boundaries (1000 and 60000)', () => { expect(stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 1000 }).success).toBe(true); expect(stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 60000 }).success).toBe(true); }); it('defaults on_error to transient', () => { const result = stepRetryConfigSchema.parse({ max_attempts: 2 }); expect(result.on_error).toBe('transient'); }); }); describe('triggerRuleSchema', () => { it('validates all trigger rules', () => { const rules = ['all_success', 'one_success', 'all_done', 'none_failed_min_one_success']; for (const rule of rules) { expect(triggerRuleSchema.safeParse(rule).success).toBe(true); } }); it('rejects invalid trigger rules', () => { const result = triggerRuleSchema.safeParse('invalid_rule'); expect(result.success).toBe(false); }); }); describe('type guards', () => { const nodes: DagNode[] = [ { ...validBase, kind: 'prompt', prompt: 'test' } as PromptNode, { ...validBase, kind: 'command', command: 'echo' } as CommandNode, { ...validBase, kind: 'bash', bash: 'echo' } as BashNode, { ...validBase, kind: 'script', script: 'print(1)', runtime: 'bun' } as ScriptNode, { ...validBase, kind: 'loop', config: { prompt: 'x', until: 'y', max_iterations: 3 } } as LoopNode, { ...validBase, kind: 'approval', message: 'ok?' } as ApprovalNode, { ...validBase, kind: 'cancel' } as CancelNode, ]; it('isPromptNode identifies prompt nodes', () => { expect(isPromptNode(nodes[0])).toBe(true); expect(isPromptNode(nodes[1])).toBe(false); }); it('isCommandNode identifies command nodes', () => { expect(isCommandNode(nodes[1])).toBe(true); expect(isCommandNode(nodes[0])).toBe(false); }); it('isBashNode identifies bash nodes', () => { expect(isBashNode(nodes[2])).toBe(true); expect(isBashNode(nodes[0])).toBe(false); }); it('isScriptNode identifies script nodes', () => { expect(isScriptNode(nodes[3])).toBe(true); expect(isScriptNode(nodes[0])).toBe(false); }); it('isLoopNode identifies loop nodes', () => { expect(isLoopNode(nodes[4])).toBe(true); expect(isLoopNode(nodes[0])).toBe(false); }); it('isApprovalNode identifies approval nodes', () => { expect(isApprovalNode(nodes[5])).toBe(true); expect(isApprovalNode(nodes[0])).toBe(false); }); it('isCancelNode identifies cancel nodes', () => { expect(isCancelNode(nodes[6])).toBe(true); expect(isCancelNode(nodes[0])).toBe(false); }); }); describe('effortLevelSchema', () => { it('validates all effort levels', () => { expect(effortLevelSchema.safeParse('low').success).toBe(true); expect(effortLevelSchema.safeParse('medium').success).toBe(true); expect(effortLevelSchema.safeParse('high').success).toBe(true); }); it('rejects invalid effort levels', () => { expect(effortLevelSchema.safeParse('extreme').success).toBe(false); }); }); describe('thinkingConfigSchema', () => { it('validates a minimal thinking config', () => { const result = thinkingConfigSchema.safeParse({}); expect(result.success).toBe(true); if (result.success) { expect(result.data.enabled).toBe(false); } }); it('validates a full thinking config', () => { const result = thinkingConfigSchema.safeParse({ enabled: true, max_tokens: 4096 }); expect(result.success).toBe(true); }); }); describe('approvalOnRejectSchema', () => { it('validates all on-reject actions', () => { expect(approvalOnRejectSchema.safeParse('retry').success).toBe(true); expect(approvalOnRejectSchema.safeParse('fail').success).toBe(true); expect(approvalOnRejectSchema.safeParse('skip').success).toBe(true); }); it('rejects invalid on-reject actions', () => { expect(approvalOnRejectSchema.safeParse('restart').success).toBe(false); }); }); describe('dagNodeBaseSchema', () => { it('validates a minimal base node', () => { const result = dagNodeBaseSchema.safeParse({ id: 'node-1', kind: 'prompt', }); expect(result.success).toBe(true); }); it('defaults depends_on to empty array', () => { const result = dagNodeBaseSchema.parse({ id: 'node-1', kind: 'prompt' }); expect(result.depends_on).toEqual([]); }); });