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.
377 lines
11 KiB
TypeScript
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([]);
|
|
});
|
|
}); |