mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-27 15:08:39 +02:00
- add shared workflow template and serialization helpers for YAML, Mermaid, and DOT export - add pattern-to-workflow upgrade and template application flows in AryxAppService - persist built-in and custom workflow templates in workspace state and expose new IPC/preload methods - cover the new backend slice with shared and app-service tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
114 lines
4.9 KiB
TypeScript
114 lines
4.9 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import { createBuiltinPatterns } from '@shared/domain/pattern';
|
|
import {
|
|
exportWorkflowDefinition,
|
|
importWorkflowDefinition,
|
|
} from '@shared/domain/workflowSerialization';
|
|
import {
|
|
buildWorkflowFromPattern,
|
|
createBuiltinWorkflowTemplates,
|
|
} from '@shared/domain/workflowTemplate';
|
|
import { validateWorkflowDefinition } from '@shared/domain/workflow';
|
|
|
|
const TIMESTAMP = '2026-04-05T00:00:00.000Z';
|
|
|
|
function requirePattern(mode: 'sequential' | 'concurrent' | 'handoff' | 'group-chat') {
|
|
const pattern = createBuiltinPatterns(TIMESTAMP).find((candidate) => candidate.mode === mode);
|
|
if (!pattern) {
|
|
throw new Error(`Expected built-in ${mode} pattern.`);
|
|
}
|
|
|
|
return pattern;
|
|
}
|
|
|
|
describe('workflow templates', () => {
|
|
test('builds a valid sequential workflow from a pattern', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('sequential'));
|
|
|
|
expect(workflow.id).toBe('workflow-sequential-review');
|
|
expect(workflow.graph.nodes.map((node) => node.kind)).toEqual(['start', 'agent', 'agent', 'agent', 'end']);
|
|
expect(workflow.graph.edges.map((edge) => `${edge.source}->${edge.target}:${edge.kind}`)).toEqual([
|
|
'start->agent-node-agent-sequential-analyst:direct',
|
|
'agent-node-agent-sequential-analyst->agent-node-agent-sequential-builder:direct',
|
|
'agent-node-agent-sequential-builder->agent-node-agent-sequential-reviewer:direct',
|
|
'agent-node-agent-sequential-reviewer->end:direct',
|
|
]);
|
|
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
|
});
|
|
|
|
test('builds a valid concurrent workflow from a pattern', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('concurrent'));
|
|
|
|
expect(workflow.graph.edges.filter((edge) => edge.kind === 'fan-out')).toHaveLength(3);
|
|
expect(workflow.graph.edges.filter((edge) => edge.kind === 'fan-in')).toHaveLength(3);
|
|
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
|
});
|
|
|
|
test('builds a valid handoff workflow from a pattern graph', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('handoff'));
|
|
|
|
expect(workflow.graph.edges.map((edge) => `${edge.source}->${edge.target}`)).toContain('start->agent-node-agent-handoff-triage');
|
|
expect(workflow.graph.edges.map((edge) => `${edge.source}->${edge.target}`)).toContain(
|
|
'agent-node-agent-handoff-triage->agent-node-agent-handoff-ux',
|
|
);
|
|
expect(workflow.graph.edges.map((edge) => `${edge.source}->${edge.target}`)).toContain(
|
|
'agent-node-agent-handoff-runtime->agent-node-agent-handoff-triage',
|
|
);
|
|
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
|
});
|
|
|
|
test('builds a group-chat workflow with a loop approximation', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('group-chat'));
|
|
const loopEdge = workflow.graph.edges.find((edge) =>
|
|
edge.source === 'agent-node-agent-group-reviewer' && edge.target === 'agent-node-agent-group-writer');
|
|
const entryEdge = workflow.graph.edges.find((edge) =>
|
|
edge.source === 'start' && edge.target === 'agent-node-agent-group-writer');
|
|
const exitEdge = workflow.graph.edges.find((edge) =>
|
|
edge.source === 'agent-node-agent-group-reviewer' && edge.target === 'end');
|
|
|
|
expect(loopEdge).toEqual(expect.objectContaining({
|
|
source: 'agent-node-agent-group-reviewer',
|
|
target: 'agent-node-agent-group-writer',
|
|
isLoop: true,
|
|
maxIterations: 5,
|
|
condition: { type: 'always' },
|
|
}));
|
|
expect(entryEdge?.isLoop).not.toBe(true);
|
|
expect(exitEdge?.isLoop).not.toBe(true);
|
|
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
|
});
|
|
|
|
test('generates built-in workflow templates for available patterns only', () => {
|
|
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
|
|
|
|
expect(templates.map((template) => template.id)).toEqual([
|
|
'workflow-template-single',
|
|
'workflow-template-sequential',
|
|
'workflow-template-concurrent',
|
|
'workflow-template-handoff',
|
|
'workflow-template-group-chat',
|
|
]);
|
|
expect(templates.every((template) => template.source === 'builtin')).toBe(true);
|
|
expect(templates.every((template) => template.category === 'orchestration')).toBe(true);
|
|
});
|
|
|
|
test('round trips workflow yaml import and export', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('sequential'));
|
|
const exported = exportWorkflowDefinition(workflow, 'yaml');
|
|
const imported = importWorkflowDefinition(exported.content, 'yaml');
|
|
|
|
expect(exported.format).toBe('yaml');
|
|
expect(imported).toEqual(workflow);
|
|
});
|
|
|
|
test('exports mermaid flowcharts with expected edges', () => {
|
|
const workflow = buildWorkflowFromPattern(requirePattern('sequential'));
|
|
const exported = exportWorkflowDefinition(workflow, 'mermaid');
|
|
|
|
expect(exported.content.startsWith('flowchart LR')).toBe(true);
|
|
expect(exported.content).toContain('-->');
|
|
expect(exported.content).toContain('n0 --> n1');
|
|
});
|
|
});
|