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.
This commit is contained in:
70
packages/ion/src/engine/__tests__/command-validation.test.ts
Normal file
70
packages/ion/src/engine/__tests__/command-validation.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isValidCommandName } from '../command-validation.js';
|
||||
|
||||
describe('isValidCommandName', () => {
|
||||
describe('valid command names', () => {
|
||||
it('accepts simple lowercase names', () => {
|
||||
expect(isValidCommandName('assist')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts kebab-case names', () => {
|
||||
expect(isValidCommandName('code-review')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts names with numbers', () => {
|
||||
expect(isValidCommandName('deploy-v2')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts single character names', () => {
|
||||
expect(isValidCommandName('a')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts names with only numbers', () => {
|
||||
expect(isValidCommandName('123')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts names with mixed alphanumeric and hyphens', () => {
|
||||
expect(isValidCommandName('a1-b2-c3')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts names starting with numbers', () => {
|
||||
expect(isValidCommandName('2fa-verify')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid command names', () => {
|
||||
it('rejects uppercase letters', () => {
|
||||
expect(isValidCommandName('Assist')).toBe(false);
|
||||
expect(isValidCommandName('CODE-REVIEW')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects leading hyphens', () => {
|
||||
expect(isValidCommandName('-assist')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects trailing hyphens', () => {
|
||||
expect(isValidCommandName('assist-')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects double hyphens', () => {
|
||||
expect(isValidCommandName('code--review')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty strings', () => {
|
||||
expect(isValidCommandName('')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects underscores', () => {
|
||||
expect(isValidCommandName('code_review')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects spaces', () => {
|
||||
expect(isValidCommandName('code review')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects special characters', () => {
|
||||
expect(isValidCommandName('code.review')).toBe(false);
|
||||
expect(isValidCommandName('code@review')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
149
packages/ion/src/engine/__tests__/condition-evaluator.test.ts
Normal file
149
packages/ion/src/engine/__tests__/condition-evaluator.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { evaluateCondition, ConditionError } from '../condition-evaluator.js';
|
||||
|
||||
describe('evaluateCondition', () => {
|
||||
describe('simple boolean conditions', () => {
|
||||
it('evaluates boolean true in a comparison', () => {
|
||||
expect(evaluateCondition('true == true', {})).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates boolean false in a comparison', () => {
|
||||
expect(evaluateCondition('false == false', {})).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates true != false', () => {
|
||||
expect(evaluateCondition('true != false', {})).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates boolean via node reference', () => {
|
||||
const outputs = { flag: { output: true } };
|
||||
expect(evaluateCondition('$flag.output == true', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('string equality with node references', () => {
|
||||
it('evaluates $nodeId.output == "value" as true when matching', () => {
|
||||
const outputs = { analysis: { output: 'done' } };
|
||||
expect(evaluateCondition('$analysis.output == "done"', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates $nodeId.output == "value" as false when not matching', () => {
|
||||
const outputs = { analysis: { output: 'pending' } };
|
||||
expect(evaluateCondition('$analysis.output == "done"', outputs)).toBe(false);
|
||||
});
|
||||
|
||||
it('evaluates $nodeId.output != "value" correctly', () => {
|
||||
const outputs = { analysis: { output: 'pending' } };
|
||||
expect(evaluateCondition('$analysis.output != "done"', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeric comparisons', () => {
|
||||
it('evaluates $score.output > 5 as true', () => {
|
||||
const outputs = { score: { output: 10 } };
|
||||
expect(evaluateCondition('$score.output > 5', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates $score.output > 5 as false when score is lower', () => {
|
||||
const outputs = { score: { output: 3 } };
|
||||
expect(evaluateCondition('$score.output > 5', outputs)).toBe(false);
|
||||
});
|
||||
|
||||
it('evaluates >= comparison', () => {
|
||||
const outputs = { score: { output: 5 } };
|
||||
expect(evaluateCondition('$score.output >= 5', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates < comparison', () => {
|
||||
const outputs = { score: { output: 3 } };
|
||||
expect(evaluateCondition('$score.output < 5', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates <= comparison', () => {
|
||||
const outputs = { score: { output: 5 } };
|
||||
expect(evaluateCondition('$score.output <= 5', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AND/OR compounds', () => {
|
||||
it('evaluates AND compound: both true', () => {
|
||||
const outputs = { a: { output: 'x' }, b: { output: 'y' } };
|
||||
expect(evaluateCondition('$a.output == "x" AND $b.output == "y"', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates AND compound: one false', () => {
|
||||
const outputs = { a: { output: 'x' }, b: { output: 'z' } };
|
||||
expect(evaluateCondition('$a.output == "x" AND $b.output == "y"', outputs)).toBe(false);
|
||||
});
|
||||
|
||||
it('evaluates OR compound: one true', () => {
|
||||
const outputs = { a: { output: 'x' }, b: { output: 'z' } };
|
||||
expect(evaluateCondition('$a.output == "x" OR $b.output == "y"', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('evaluates OR compound: both false', () => {
|
||||
const outputs = { a: { output: 'z' }, b: { output: 'z' } };
|
||||
expect(evaluateCondition('$a.output == "x" OR $b.output == "y"', outputs)).toBe(false);
|
||||
});
|
||||
|
||||
it('evaluates mixed AND/OR with correct precedence', () => {
|
||||
const outputs = { a: { output: 'x' }, b: { output: 'y' }, c: { output: 'z' } };
|
||||
// false AND true OR true => false OR true => true (AND binds tighter)
|
||||
expect(evaluateCondition('$a.output == "wrong" AND $b.output == "y" OR $c.output == "z"', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parenthesized expressions', () => {
|
||||
it('evaluates parenthesized expressions', () => {
|
||||
const outputs = { a: { output: 'x' }, b: { output: 'y' } };
|
||||
expect(evaluateCondition('($a.output == "x" OR $a.output == "z") AND $b.output == "y"', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws ConditionError on invalid expressions', () => {
|
||||
expect(() => evaluateCondition('!!!invalid', {})).toThrow(ConditionError);
|
||||
});
|
||||
|
||||
it('throws ConditionError on missing node reference', () => {
|
||||
expect(() => evaluateCondition('$missing.output == "x"', {})).toThrow(ConditionError);
|
||||
});
|
||||
|
||||
it('throws ConditionError on node reference without field', () => {
|
||||
expect(() => evaluateCondition('$analysis == "x"', {})).toThrow(ConditionError);
|
||||
});
|
||||
|
||||
it('throws ConditionError on unterminated string', () => {
|
||||
expect(() => evaluateCondition('"unterminated', {})).toThrow(ConditionError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespace handling', () => {
|
||||
it('handles extra whitespace around operators', () => {
|
||||
const outputs = { a: { output: 'x' } };
|
||||
expect(evaluateCondition(' $a.output == "x" ', outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quoted strings with special characters', () => {
|
||||
it('handles double-quoted strings with spaces', () => {
|
||||
const outputs = { msg: { output: 'hello world' } };
|
||||
expect(evaluateCondition('$msg.output == "hello world"', outputs)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles single-quoted strings', () => {
|
||||
const outputs = { msg: { output: 'hello' } };
|
||||
expect(evaluateCondition("$msg.output == 'hello'", outputs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty condition', () => {
|
||||
it('returns true for empty string', () => {
|
||||
expect(evaluateCondition('', {})).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for whitespace-only string', () => {
|
||||
expect(evaluateCondition(' ', {})).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
132
packages/ion/src/engine/__tests__/output-ref.test.ts
Normal file
132
packages/ion/src/engine/__tests__/output-ref.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
resolveNodeOutputField,
|
||||
declaredFieldsFromSchema,
|
||||
OutputRefError,
|
||||
} from '../output-ref.js';
|
||||
|
||||
describe('resolveNodeOutputField', () => {
|
||||
describe('with declared schema match', () => {
|
||||
it('returns value when field exists in output and schema', () => {
|
||||
const declaredFields = new Set(['name', 'status']);
|
||||
const output = { name: 'test-result', status: 'completed' };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields);
|
||||
expect(result).toEqual({ kind: 'value', value: 'test-result' });
|
||||
});
|
||||
|
||||
it('returns JSON-serialized value for non-string fields', () => {
|
||||
const declaredFields = new Set(['count']);
|
||||
const output = { count: 42 };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'count', declaredFields);
|
||||
expect(result).toEqual({ kind: 'value', value: '42' });
|
||||
});
|
||||
|
||||
it('returns JSON-serialized value for object fields', () => {
|
||||
const declaredFields = new Set(['data']);
|
||||
const output = { data: { key: 'val' } };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'data', declaredFields);
|
||||
expect(result).toEqual({ kind: 'value', value: '{"key":"val"}' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with schemaless JSON output', () => {
|
||||
it('returns value when field exists in output without schema', () => {
|
||||
const output = { dynamic_field: 'hello' };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'dynamic_field');
|
||||
expect(result).toEqual({ kind: 'value', value: 'hello' });
|
||||
});
|
||||
|
||||
it('returns JSON-serialized number without schema', () => {
|
||||
const output = { score: 99 };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'score');
|
||||
expect(result).toEqual({ kind: 'value', value: '99' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing optional field', () => {
|
||||
it('returns empty when field is declared in schema but missing from output', () => {
|
||||
const declaredFields = new Set(['name', 'optional_field']);
|
||||
const output = { name: 'test' };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'optional_field', declaredFields);
|
||||
expect(result).toEqual({ kind: 'empty', value: '' });
|
||||
});
|
||||
|
||||
it('returns empty when field exists but value is null', () => {
|
||||
const declaredFields = new Set(['name']);
|
||||
const output = { name: null };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields);
|
||||
expect(result).toEqual({ kind: 'empty', value: '' });
|
||||
});
|
||||
|
||||
it('returns empty when field exists but value is undefined', () => {
|
||||
const declaredFields = new Set(['name']);
|
||||
const output = { name: undefined };
|
||||
const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields);
|
||||
expect(result).toEqual({ kind: 'empty', value: '' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing required field', () => {
|
||||
it('throws OutputRefError when field is not declared and not in output', () => {
|
||||
const output = { name: 'test' };
|
||||
expect(() => resolveNodeOutputField(output, 'node-1', 'nonexistent')).toThrow(OutputRefError);
|
||||
});
|
||||
|
||||
it('throws OutputRefError with nodeId and field info', () => {
|
||||
const output = { name: 'test' };
|
||||
try {
|
||||
resolveNodeOutputField(output, 'my-node', 'missing_field');
|
||||
expect.unreachable('Should have thrown');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(OutputRefError);
|
||||
if (err instanceof OutputRefError) {
|
||||
expect(err.nodeId).toBe('my-node');
|
||||
expect(err.field).toBe('missing_field');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('includes available fields in error message', () => {
|
||||
const output = { name: 'test', status: 'ok' };
|
||||
try {
|
||||
resolveNodeOutputField(output, 'node-1', 'nonexistent');
|
||||
expect.unreachable('Should have thrown');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(OutputRefError);
|
||||
if (err instanceof OutputRefError) {
|
||||
expect(err.message).toContain('name');
|
||||
expect(err.message).toContain('status');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('declaredFieldsFromSchema', () => {
|
||||
it('extracts fields from a valid JSON Schema object', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
count: { type: 'number' },
|
||||
},
|
||||
};
|
||||
const fields = declaredFieldsFromSchema(schema);
|
||||
expect(fields).toEqual(new Set(['name', 'count']));
|
||||
});
|
||||
|
||||
it('returns empty set for undefined schema', () => {
|
||||
const fields = declaredFieldsFromSchema(undefined);
|
||||
expect(fields).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('returns empty set for string schema', () => {
|
||||
const fields = declaredFieldsFromSchema('just a string description');
|
||||
expect(fields).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('returns empty set for schema without properties', () => {
|
||||
const fields = declaredFieldsFromSchema({ type: 'object' });
|
||||
expect(fields).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user