Files
aryx/tests/shared/workflowTemplate.test.ts
T
David KayaandCopilot 4971dcf9fc feat(workflows): add phase 5 backend templates and export support
- 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>
2026-04-05 21:11:59 +02:00

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');
});
});