Files
aryx/tests/shared/workflowTemplate.test.ts
T
David KayaandCopilot b302ea5979 feat(workflows): replace basic templates with 8 rich hand-crafted templates
Replace auto-generated pattern-based templates with complex, real-world
workflow templates showcasing all node types and edge features:

- Code Review Pipeline: sequential chain with checkpointing
- Research & Summarize: 3 parallel agents via fan-out/fan-in
- Customer Support Triage: conditional routing with expression edges
- Content Creation Pipeline: writer/editor revision loop
- Multi-Agent Debate: proposer/critic debate loop
- Data Processing with Validation: invoke-function nodes, state scopes
- Approval Workflow: request-port for human-in-the-loop review
- Nested Workflow Orchestrator: inline sub-workflow composition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-05 22:27:35 +02:00

194 lines
8.2 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('creates 8 hand-crafted builtin workflow templates', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
expect(templates).toHaveLength(8);
expect(templates.map((template) => template.id)).toEqual([
'workflow-template-code-review',
'workflow-template-research-summarize',
'workflow-template-customer-support',
'workflow-template-content-creation',
'workflow-template-multi-agent-debate',
'workflow-template-data-processing',
'workflow-template-approval',
'workflow-template-nested-orchestrator',
]);
expect(templates.every((template) => template.source === 'builtin')).toBe(true);
});
test('builtin templates span all categories', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const categories = new Set(templates.map((template) => template.category));
expect(categories).toContain('orchestration');
expect(categories).toContain('data-pipeline');
expect(categories).toContain('human-in-loop');
});
test('builtin templates produce valid workflows', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
for (const template of templates) {
const issues = validateWorkflowDefinition(template.workflow);
expect(issues.filter((issue) => issue.level === 'error')).toEqual([]);
}
});
test('builtin templates use the provided timestamp', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
for (const template of templates) {
expect(template.createdAt).toBe(TIMESTAMP);
expect(template.updatedAt).toBe(TIMESTAMP);
}
});
test('research-summarize template uses fan-out and fan-in edges', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const research = templates.find((t) => t.id === 'workflow-template-research-summarize')!;
expect(research.workflow.graph.edges.filter((e) => e.kind === 'fan-out')).toHaveLength(3);
expect(research.workflow.graph.edges.filter((e) => e.kind === 'fan-in')).toHaveLength(3);
});
test('content-creation template has a loop edge with maxIterations', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const content = templates.find((t) => t.id === 'workflow-template-content-creation')!;
const loopEdge = content.workflow.graph.edges.find((e) => e.isLoop && e.source === 'editor' && e.target === 'writer');
expect(loopEdge).toBeDefined();
expect(loopEdge!.maxIterations).toBe(3);
expect(loopEdge!.source).toBe('editor');
expect(loopEdge!.target).toBe('writer');
});
test('data-processing template includes invoke-function nodes', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const dataProc = templates.find((t) => t.id === 'workflow-template-data-processing')!;
const funcNodes = dataProc.workflow.graph.nodes.filter((n) => n.kind === 'invoke-function');
expect(funcNodes).toHaveLength(2);
});
test('approval template includes a request-port node', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const approval = templates.find((t) => t.id === 'workflow-template-approval')!;
const portNode = approval.workflow.graph.nodes.find((n) => n.kind === 'request-port');
expect(portNode).toBeDefined();
expect(portNode!.config).toEqual(expect.objectContaining({
kind: 'request-port',
portId: 'review',
requestType: 'ReviewRequest',
responseType: 'ReviewDecision',
}));
});
test('nested-orchestrator template includes a sub-workflow node', () => {
const templates = createBuiltinWorkflowTemplates(TIMESTAMP);
const nested = templates.find((t) => t.id === 'workflow-template-nested-orchestrator')!;
const subNode = nested.workflow.graph.nodes.find((n) => n.kind === 'sub-workflow');
expect(subNode).toBeDefined();
expect(subNode!.config).toEqual(expect.objectContaining({ kind: 'sub-workflow' }));
});
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');
});
});