mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-29 16:08:46 +02:00
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>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user