mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-04 19:08:40 +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:
@@ -77,6 +77,7 @@ function createWorkspace(overrides?: Partial<WorkspaceState>): WorkspaceState {
|
||||
projects: [createProject(), createProject({ id: 'project-scratchpad', name: 'Scratchpad', path: 'C:\\scratchpad' })],
|
||||
patterns: [createPattern(), createPattern({ id: 'pattern-single-chat', name: '1-on-1 Copilot Chat', mode: 'single' })],
|
||||
workflows: [],
|
||||
workflowTemplates: [],
|
||||
settings: createWorkspaceSettings(),
|
||||
sessions: [
|
||||
createSession(),
|
||||
@@ -117,6 +118,7 @@ function createWorkspace(overrides?: Partial<WorkspaceState>): WorkspaceState {
|
||||
return {
|
||||
...workspace,
|
||||
workflows: overrides?.workflows ?? workspace.workflows,
|
||||
workflowTemplates: overrides?.workflowTemplates ?? workspace.workflowTemplates,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
import { createWorkspaceSeed } from '@shared/domain/workspace';
|
||||
|
||||
describe('workspace seed', () => {
|
||||
test('starts empty and seeds the built-in orchestration patterns with a shared timestamp', () => {
|
||||
test('starts empty and seeds built-in patterns and workflow templates with a shared timestamp', () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
|
||||
expect(workspace.projects).toEqual([]);
|
||||
@@ -30,6 +30,13 @@ describe('workspace seed', () => {
|
||||
'group-chat',
|
||||
'magentic',
|
||||
]);
|
||||
expect(workspace.workflowTemplates.map((template) => template.id)).toEqual([
|
||||
'workflow-template-single',
|
||||
'workflow-template-sequential',
|
||||
'workflow-template-concurrent',
|
||||
'workflow-template-handoff',
|
||||
'workflow-template-group-chat',
|
||||
]);
|
||||
|
||||
for (const pattern of workspace.patterns) {
|
||||
expect(pattern.createdAt).toBe(workspace.lastUpdatedAt);
|
||||
@@ -41,5 +48,9 @@ describe('workspace seed', () => {
|
||||
|
||||
expect(magentic?.availability).toBe('unavailable');
|
||||
expect(magentic?.unavailabilityReason).toContain('unsupported');
|
||||
for (const template of workspace.workflowTemplates) {
|
||||
expect(template.createdAt).toBe(workspace.lastUpdatedAt);
|
||||
expect(template.updatedAt).toBe(workspace.lastUpdatedAt);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user