Files
boocode/packages/ion/src/schema/__tests__/dag-node.test.ts
indifferentketchup 02063072ab chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
2026-06-07 22:16:45 +00:00

377 lines
11 KiB
TypeScript

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([]);
});
});