mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-28 23:48:39 +02:00
refactor: remove legacy patterns system, unify on workflows
Remove the entire patterns domain model, IPC channels, sidecar services, renderer components, and tests. Sessions now bind exclusively to workflows via workflowId. Builtin workflows replace builtin patterns. Backend: - Make AgentNodeConfig standalone (no longer extends PatternAgentDefinition) - Add WorkflowOrchestrationMode and WorkflowExecutionDefinition - Create builtin workflows (single-agent, sequential, concurrent, handoff, group-chat) - Rewrite session model config helpers for workflow-only - Remove pattern IPC channels, handlers, and preload bindings - Merge createSession/createWorkflowSession into single method - Remove sidecar PatternGraphResolver, PatternValidator, pattern DTOs - Add workspace migration for legacy sessions (patternId -> workflowId) Frontend: - Delete PatternEditor, pattern-graph components, patternGraph lib - Delete NewSessionModal (session creation uses workflows directly) - Remove PatternsSection from SettingsPanel - Update App.tsx, ChatPane, ActivityPanel, Sidebar, RunTimeline, AgentConfigFields, InlinePills, sessionActivity to use workflow types - Delete pattern.ts domain module 78 files changed across backend and frontend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+40
-20
@@ -5,10 +5,10 @@ import {
|
||||
buildAvailableModelCatalog,
|
||||
findModel,
|
||||
findModelByReference,
|
||||
normalizePatternModels,
|
||||
normalizeWorkflowModels,
|
||||
resolveReasoningEffort,
|
||||
} from '@shared/domain/models';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
|
||||
const availableModels: SidecarModelCapability[] = [
|
||||
{
|
||||
@@ -24,24 +24,43 @@ const availableModels: SidecarModelCapability[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function createPattern(): PatternDefinition {
|
||||
function createWorkflow(): WorkflowDefinition {
|
||||
return {
|
||||
id: 'pattern-1',
|
||||
name: 'Pattern',
|
||||
description: '',
|
||||
mode: 'single',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Primary Agent',
|
||||
description: 'Helpful assistant',
|
||||
instructions: 'Help the user.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-1',
|
||||
kind: 'agent',
|
||||
label: 'Primary Agent',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-1',
|
||||
name: 'Primary Agent',
|
||||
description: 'Helpful assistant',
|
||||
instructions: 'Help the user.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
},
|
||||
{ id: 'end', kind: 'end', label: 'End', position: { x: 400, y: 0 }, config: { kind: 'end' } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge-start-agent', source: 'start', target: 'agent-1', kind: 'direct' },
|
||||
{ id: 'edge-agent-end', source: 'agent-1', target: 'end', kind: 'direct' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
checkpointing: { enabled: false },
|
||||
executionMode: 'off-thread',
|
||||
orchestrationMode: 'single',
|
||||
maxIterations: 1,
|
||||
},
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||
};
|
||||
@@ -70,12 +89,13 @@ describe('dynamic model catalog', () => {
|
||||
expect(findModelByReference('Claude Sonnet 4.5', catalog)?.id).toBe('claude-sonnet-4.5');
|
||||
});
|
||||
|
||||
test('normalizes pattern agents before runtime execution', () => {
|
||||
const normalized = normalizePatternModels(
|
||||
createPattern(),
|
||||
test('normalizes workflow agent reasoning effort before runtime execution', () => {
|
||||
const normalized = normalizeWorkflowModels(
|
||||
createWorkflow(),
|
||||
buildAvailableModelCatalog(availableModels),
|
||||
);
|
||||
|
||||
expect(normalized.agents[0].reasoningEffort).toBeUndefined();
|
||||
const primaryAgent = normalized.graph.nodes.find((node) => node.kind === 'agent');
|
||||
expect(primaryAgent?.config.kind === 'agent' ? primaryAgent.config.reasoningEffort : undefined).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import {
|
||||
appendRunActivityEvent,
|
||||
completeSessionRunRecord,
|
||||
@@ -12,30 +12,58 @@ import {
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { PendingApprovalRecord } from '@shared/domain/approval';
|
||||
|
||||
function createPattern(): PatternDefinition {
|
||||
function createWorkflow(): WorkflowDefinition {
|
||||
return {
|
||||
id: 'pattern-sequential',
|
||||
id: 'workflow-sequential',
|
||||
name: 'Sequential Trio Review',
|
||||
description: 'Sequential handoff review flow.',
|
||||
mode: 'sequential',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-writer',
|
||||
name: 'Writer',
|
||||
description: 'Writes the draft.',
|
||||
instructions: 'Write.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
{
|
||||
id: 'agent-reviewer',
|
||||
name: 'Reviewer',
|
||||
description: 'Reviews the draft.',
|
||||
instructions: 'Review.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-writer',
|
||||
kind: 'agent',
|
||||
label: 'Writer',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-writer',
|
||||
name: 'Writer',
|
||||
description: 'Writes the draft.',
|
||||
instructions: 'Write.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'agent-reviewer',
|
||||
kind: 'agent',
|
||||
label: 'Reviewer',
|
||||
position: { x: 400, y: 0 },
|
||||
order: 1,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-reviewer',
|
||||
name: 'Reviewer',
|
||||
description: 'Reviews the draft.',
|
||||
instructions: 'Review.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
},
|
||||
},
|
||||
{ id: 'end', kind: 'end', label: 'End', position: { x: 600, y: 0 }, config: { kind: 'end' } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge-start-writer', source: 'start', target: 'agent-writer', kind: 'direct' },
|
||||
{ id: 'edge-writer-reviewer', source: 'agent-writer', target: 'agent-reviewer', kind: 'direct' },
|
||||
{ id: 'edge-reviewer-end', source: 'agent-reviewer', target: 'end', kind: 'direct' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
checkpointing: { enabled: false },
|
||||
executionMode: 'off-thread',
|
||||
orchestrationMode: 'sequential',
|
||||
maxIterations: 1,
|
||||
},
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||
};
|
||||
@@ -57,7 +85,7 @@ describe('run timeline helpers', () => {
|
||||
project: createProject(),
|
||||
workingDirectory: 'C:\\workspace\\alpha\\packages\\app',
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
preRunGitBaselineFiles: [
|
||||
@@ -73,9 +101,9 @@ describe('run timeline helpers', () => {
|
||||
projectId: 'project-1',
|
||||
projectPath: 'C:\\workspace\\alpha',
|
||||
workingDirectory: 'C:\\workspace\\alpha\\packages\\app',
|
||||
patternId: 'pattern-sequential',
|
||||
patternName: 'Sequential Trio Review',
|
||||
patternMode: 'sequential',
|
||||
workflowId: 'workflow-sequential',
|
||||
workflowName: 'Sequential Trio Review',
|
||||
workflowMode: 'sequential',
|
||||
triggerMessageId: 'msg-user-1',
|
||||
status: 'running',
|
||||
});
|
||||
@@ -112,7 +140,7 @@ describe('run timeline helpers', () => {
|
||||
requestId: 'turn-1',
|
||||
project: createProject(),
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
});
|
||||
@@ -159,7 +187,7 @@ describe('run timeline helpers', () => {
|
||||
requestId: 'turn-1',
|
||||
project: createProject(),
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
}),
|
||||
@@ -187,7 +215,7 @@ describe('run timeline helpers', () => {
|
||||
requestId: 'turn-1',
|
||||
project: createProject(),
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
});
|
||||
@@ -242,7 +270,7 @@ describe('run timeline helpers', () => {
|
||||
requestId: 'turn-1',
|
||||
project: createProject(),
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import {
|
||||
applySessionApprovalSettings,
|
||||
applySessionModelConfig,
|
||||
@@ -12,42 +12,70 @@ import {
|
||||
type SessionRecord,
|
||||
} from '@shared/domain/session';
|
||||
|
||||
function createPattern(): PatternDefinition {
|
||||
function createWorkflow(): WorkflowDefinition {
|
||||
return {
|
||||
id: 'pattern-single',
|
||||
id: 'workflow-single',
|
||||
name: '1-on-1 Copilot Chat',
|
||||
description: 'Single agent chat',
|
||||
mode: 'single',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-primary',
|
||||
name: 'Primary Agent',
|
||||
description: 'Helpful assistant',
|
||||
instructions: 'Help the user.',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
{
|
||||
id: 'agent-secondary',
|
||||
name: 'Secondary Agent',
|
||||
description: 'Unused here',
|
||||
instructions: 'Review.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-primary-node',
|
||||
kind: 'agent',
|
||||
label: 'Primary Agent',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-primary',
|
||||
name: 'Primary Agent',
|
||||
description: 'Helpful assistant',
|
||||
instructions: 'Help the user.',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'agent-secondary-node',
|
||||
kind: 'agent',
|
||||
label: 'Secondary Agent',
|
||||
position: { x: 400, y: 0 },
|
||||
order: 1,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-secondary',
|
||||
name: 'Secondary Agent',
|
||||
description: 'Unused here',
|
||||
instructions: 'Review.',
|
||||
model: 'claude-sonnet-4.5',
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
},
|
||||
{ id: 'end', kind: 'end', label: 'End', position: { x: 600, y: 0 }, config: { kind: 'end' } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge-start-primary', source: 'start', target: 'agent-primary-node', kind: 'direct' },
|
||||
{ id: 'edge-primary-secondary', source: 'agent-primary-node', target: 'agent-secondary-node', kind: 'direct' },
|
||||
{ id: 'edge-secondary-end', source: 'agent-secondary-node', target: 'end', kind: 'direct' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
checkpointing: { enabled: false },
|
||||
executionMode: 'off-thread',
|
||||
orchestrationMode: 'single',
|
||||
maxIterations: 1,
|
||||
},
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-1',
|
||||
projectId: 'project-scratchpad',
|
||||
patternId: 'pattern-single',
|
||||
return {
|
||||
id: 'session-1',
|
||||
projectId: 'project-scratchpad',
|
||||
workflowId: 'workflow-single',
|
||||
title: 'Scratchpad',
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||
@@ -60,13 +88,13 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
|
||||
describe('session model config helpers', () => {
|
||||
test('captures the initial model settings from the primary agent', () => {
|
||||
expect(createSessionModelConfig(createPattern())).toEqual({
|
||||
expect(createSessionModelConfig(createWorkflow())).toEqual({
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves persisted session overrides over the pattern defaults', () => {
|
||||
test('resolves persisted session overrides over the workflow defaults', () => {
|
||||
const config = resolveSessionModelConfig(
|
||||
createSession({
|
||||
sessionModelConfig: {
|
||||
@@ -74,7 +102,7 @@ describe('session model config helpers', () => {
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
}),
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
);
|
||||
|
||||
expect(config).toEqual({
|
||||
@@ -84,9 +112,9 @@ describe('session model config helpers', () => {
|
||||
});
|
||||
|
||||
test('applies session model settings only to the primary agent', () => {
|
||||
const pattern = createPattern();
|
||||
const workflow = createWorkflow();
|
||||
const updated = applySessionModelConfig(
|
||||
pattern,
|
||||
workflow,
|
||||
createSession({
|
||||
sessionModelConfig: {
|
||||
model: 'gpt-5.4-mini',
|
||||
@@ -95,22 +123,25 @@ describe('session model config helpers', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updated.agents[0].model).toBe('gpt-5.4-mini');
|
||||
expect(updated.agents[0].reasoningEffort).toBe('low');
|
||||
expect(updated.agents[1]).toEqual(pattern.agents[1]);
|
||||
const primaryAgent = updated.graph.nodes[1];
|
||||
const secondaryAgent = updated.graph.nodes[2];
|
||||
const originalSecondaryAgent = workflow.graph.nodes[2];
|
||||
expect(primaryAgent?.config.kind === 'agent' ? primaryAgent.config.model : undefined).toBe('gpt-5.4-mini');
|
||||
expect(primaryAgent?.config.kind === 'agent' ? primaryAgent.config.reasoningEffort : undefined).toBe('low');
|
||||
expect(secondaryAgent).toEqual(originalSecondaryAgent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session title helpers', () => {
|
||||
test('keeps a manual title instead of recomputing it from the first user message', () => {
|
||||
const pattern = createPattern();
|
||||
const workflow = createWorkflow();
|
||||
const session = createSession({
|
||||
title: 'Release readiness review',
|
||||
titleSource: 'manual',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveSessionTitle(session, pattern, [
|
||||
resolveSessionTitle(session, workflow, [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
@@ -123,11 +154,11 @@ describe('session title helpers', () => {
|
||||
});
|
||||
|
||||
test('builds auto titles from markdown-heavy first messages', () => {
|
||||
const pattern = createPattern();
|
||||
const workflow = createWorkflow();
|
||||
const session = createSession();
|
||||
|
||||
expect(
|
||||
resolveSessionTitle(session, pattern, [
|
||||
resolveSessionTitle(session, workflow, [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
@@ -164,25 +195,28 @@ describe('session tooling helpers', () => {
|
||||
});
|
||||
|
||||
describe('session approval helpers', () => {
|
||||
test('normalizes session approval overrides and applies them over pattern defaults', () => {
|
||||
const pattern = {
|
||||
...createPattern(),
|
||||
approvalPolicy: {
|
||||
rules: [{ kind: 'tool-call' as const }],
|
||||
autoApprovedToolNames: ['git.status'],
|
||||
test('normalizes session approval overrides and applies them over workflow defaults', () => {
|
||||
const workflow = {
|
||||
...createWorkflow(),
|
||||
settings: {
|
||||
...createWorkflow().settings,
|
||||
approvalPolicy: {
|
||||
rules: [{ kind: 'tool-call' as const }],
|
||||
autoApprovedToolNames: ['git.status'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveSessionApprovalSettings(createSession())).toBeUndefined();
|
||||
expect(
|
||||
applySessionApprovalSettings(
|
||||
pattern,
|
||||
createSession({
|
||||
applySessionApprovalSettings(
|
||||
workflow,
|
||||
createSession({
|
||||
approvalSettings: {
|
||||
autoApprovedToolNames: ['git.diff', ' git.diff '],
|
||||
},
|
||||
}),
|
||||
).approvalPolicy,
|
||||
).settings.approvalPolicy,
|
||||
).toEqual({
|
||||
rules: [{ kind: 'tool-call' }],
|
||||
autoApprovedToolNames: ['git.diff'],
|
||||
|
||||
@@ -9,30 +9,49 @@ import {
|
||||
renameSessionRecord,
|
||||
setSessionMessagePinnedRecord,
|
||||
} from '@shared/domain/sessionLibrary';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { createWorkspaceSettings } from '@shared/domain/tooling';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
function createPattern(overrides?: Partial<PatternDefinition>): PatternDefinition {
|
||||
function createWorkflow(overrides?: Partial<WorkflowDefinition>): WorkflowDefinition {
|
||||
return {
|
||||
id: 'pattern-sequential-review',
|
||||
id: 'workflow-sequential-review',
|
||||
name: 'Sequential Review',
|
||||
description: 'Multi-agent review workflow.',
|
||||
mode: 'sequential',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-analyst',
|
||||
name: 'Analyst',
|
||||
description: 'Reviews the request and finds issues.',
|
||||
instructions: 'Analyze the request.',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-analyst-node',
|
||||
kind: 'agent',
|
||||
label: 'Analyst',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-analyst',
|
||||
name: 'Analyst',
|
||||
description: 'Reviews the request and finds issues.',
|
||||
instructions: 'Analyze the request.',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
},
|
||||
{ id: 'end', kind: 'end', label: 'End', position: { x: 400, y: 0 }, config: { kind: 'end' } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge-start-agent', source: 'start', target: 'agent-analyst-node', kind: 'direct' },
|
||||
{ id: 'edge-agent-end', source: 'agent-analyst-node', target: 'end', kind: 'direct' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
checkpointing: { enabled: false },
|
||||
executionMode: 'off-thread',
|
||||
orchestrationMode: 'sequential',
|
||||
maxIterations: 1,
|
||||
},
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||
...overrides,
|
||||
@@ -53,7 +72,7 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-1',
|
||||
projectId: 'project-alpha',
|
||||
patternId: 'pattern-sequential-review',
|
||||
workflowId: 'workflow-sequential-review',
|
||||
title: 'Investigate Copilot refresh bug',
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
updatedAt: '2026-03-23T00:05:00.000Z',
|
||||
@@ -75,8 +94,7 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createWorkspace(overrides?: Partial<WorkspaceState>): WorkspaceState {
|
||||
const workspace: 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: [],
|
||||
workflows: [createWorkflow(), createWorkflow({ id: 'workflow-single-chat', name: '1-on-1 Copilot Chat', settings: { checkpointing: { enabled: false }, executionMode: 'off-thread', orchestrationMode: 'single', maxIterations: 1 } })],
|
||||
workflowTemplates: [],
|
||||
settings: createWorkspaceSettings(),
|
||||
sessions: [
|
||||
@@ -84,7 +102,7 @@ function createWorkspace(overrides?: Partial<WorkspaceState>): WorkspaceState {
|
||||
createSession({
|
||||
id: 'session-2',
|
||||
projectId: 'project-scratchpad',
|
||||
patternId: 'pattern-single-chat',
|
||||
workflowId: 'workflow-single-chat',
|
||||
title: 'Scratchpad brainstorm',
|
||||
status: 'running',
|
||||
updatedAt: '2026-03-23T00:10:00.000Z',
|
||||
@@ -109,7 +127,7 @@ function createWorkspace(overrides?: Partial<WorkspaceState>): WorkspaceState {
|
||||
}),
|
||||
],
|
||||
selectedProjectId: 'project-alpha',
|
||||
selectedPatternId: 'pattern-sequential-review',
|
||||
selectedWorkflowId: 'workflow-sequential-review',
|
||||
selectedSessionId: 'session-1',
|
||||
lastUpdatedAt: '2026-03-23T00:10:00.000Z',
|
||||
...overrides,
|
||||
@@ -291,7 +309,7 @@ describe('session library helpers', () => {
|
||||
|
||||
const branch = branchSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-branch',
|
||||
'msg-3',
|
||||
'2026-03-23T00:04:00.000Z',
|
||||
@@ -341,7 +359,7 @@ describe('session library helpers', () => {
|
||||
expect(() =>
|
||||
branchSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-branch',
|
||||
'msg-1',
|
||||
'2026-03-23T00:04:00.000Z',
|
||||
@@ -384,7 +402,7 @@ describe('session library helpers', () => {
|
||||
|
||||
const branch = branchSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-branch',
|
||||
'msg-2',
|
||||
'2026-03-23T00:05:00.000Z',
|
||||
@@ -436,7 +454,7 @@ describe('session library helpers', () => {
|
||||
|
||||
const regenerated = regenerateSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-regenerated',
|
||||
'msg-4',
|
||||
'2026-03-23T00:06:00.000Z',
|
||||
@@ -483,7 +501,7 @@ describe('session library helpers', () => {
|
||||
expect(() =>
|
||||
regenerateSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-regenerated',
|
||||
'msg-2',
|
||||
'2026-03-23T00:06:00.000Z',
|
||||
@@ -541,7 +559,7 @@ describe('session library helpers', () => {
|
||||
|
||||
const edited = editAndResendSessionRecord(
|
||||
sourceSession,
|
||||
createPattern(),
|
||||
createWorkflow(),
|
||||
'session-edited',
|
||||
'msg-3',
|
||||
'Focus on session state only.',
|
||||
@@ -585,7 +603,7 @@ describe('session library helpers', () => {
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
score: 31,
|
||||
matchedFields: ['title', 'message', 'project', 'pattern'],
|
||||
matchedFields: ['title', 'message', 'project', 'workflow'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
buildWorkflowExecutionPattern,
|
||||
buildWorkflowExecutionDefinition,
|
||||
validateWorkflowDefinition,
|
||||
type WorkflowDefinition,
|
||||
} from '@shared/domain/workflow';
|
||||
@@ -245,16 +245,16 @@ describe('workflow validation', () => {
|
||||
expect(issues.some((issue) => issue.message.includes('exactly one of workflowId or inlineWorkflow'))).toBe(true);
|
||||
});
|
||||
|
||||
test('builds a synthetic execution pattern from workflow agents', () => {
|
||||
const pattern = buildWorkflowExecutionPattern(createWorkflow());
|
||||
test('builds an execution definition from workflow agents', () => {
|
||||
const execution = buildWorkflowExecutionDefinition(createWorkflow());
|
||||
|
||||
expect(pattern.id).toBe('workflow-1');
|
||||
expect(pattern.agents).toHaveLength(1);
|
||||
expect(pattern.agents[0]?.name).toBe('Primary Agent');
|
||||
expect(pattern.graph?.nodes.map((node) => node.kind)).toEqual(['user-input', 'agent', 'user-output']);
|
||||
expect(execution.id).toBe('workflow-1');
|
||||
expect(execution.agents).toHaveLength(1);
|
||||
expect(execution.agents[0]?.name).toBe('Primary Agent');
|
||||
expect(execution.orchestrationMode).toBe('single');
|
||||
});
|
||||
|
||||
test('includes referenced sub-workflow agents when building execution patterns', () => {
|
||||
test('includes referenced sub-workflow agents when building execution definitions', () => {
|
||||
const childWorkflow = createReferencedSubWorkflow();
|
||||
const workflow = createWorkflow();
|
||||
workflow.graph.nodes[1] = {
|
||||
@@ -270,12 +270,12 @@ describe('workflow validation', () => {
|
||||
workflow.graph.edges[0] = { id: 'edge-start-sub', source: 'start', target: 'sub-workflow', kind: 'direct' };
|
||||
workflow.graph.edges[1] = { id: 'edge-sub-end', source: 'sub-workflow', target: 'end', kind: 'direct' };
|
||||
|
||||
const pattern = buildWorkflowExecutionPattern(workflow, {
|
||||
resolveWorkflow: (workflowId) => workflowId === childWorkflow.id ? childWorkflow : undefined,
|
||||
const execution = buildWorkflowExecutionDefinition(workflow, {
|
||||
resolveWorkflow: (workflowId: string) => workflowId === childWorkflow.id ? childWorkflow : undefined,
|
||||
});
|
||||
|
||||
expect(pattern.mode).toBe('single');
|
||||
expect(pattern.agents.map((agent) => agent.id)).toEqual(['agent-reviewer']);
|
||||
expect(execution.orchestrationMode).toBe('single');
|
||||
expect(execution.agents.map((agent) => agent.id)).toEqual(['agent-reviewer']);
|
||||
});
|
||||
|
||||
test('accepts simple property and expression conditions', () => {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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 built-in patterns and workflow templates with a shared timestamp', () => {
|
||||
test('starts empty and seeds built-in workflows and workflow templates with a shared timestamp', () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
|
||||
expect(workspace.projects).toEqual([]);
|
||||
@@ -19,16 +19,15 @@ describe('workspace seed', () => {
|
||||
},
|
||||
});
|
||||
expect(workspace.selectedProjectId).toBeUndefined();
|
||||
expect(workspace.selectedPatternId).toBeUndefined();
|
||||
expect(workspace.selectedWorkflowId).toBeUndefined();
|
||||
expect(workspace.selectedSessionId).toBeUndefined();
|
||||
|
||||
expect(workspace.patterns.map((pattern) => pattern.mode)).toEqual([
|
||||
expect(workspace.workflows.map((workflow) => workflow.settings.orchestrationMode)).toEqual([
|
||||
'single',
|
||||
'sequential',
|
||||
'concurrent',
|
||||
'handoff',
|
||||
'group-chat',
|
||||
'magentic',
|
||||
]);
|
||||
expect(workspace.workflowTemplates.map((template) => template.id)).toEqual([
|
||||
'workflow-template-code-review',
|
||||
@@ -41,16 +40,12 @@ describe('workspace seed', () => {
|
||||
'workflow-template-nested-orchestrator',
|
||||
]);
|
||||
|
||||
for (const pattern of workspace.patterns) {
|
||||
expect(pattern.createdAt).toBe(workspace.lastUpdatedAt);
|
||||
expect(pattern.updatedAt).toBe(workspace.lastUpdatedAt);
|
||||
expect(pattern.approvalPolicy?.rules).toContainEqual({ kind: 'tool-call' });
|
||||
for (const workflow of workspace.workflows) {
|
||||
expect(workflow.createdAt).toBe(workspace.lastUpdatedAt);
|
||||
expect(workflow.updatedAt).toBe(workspace.lastUpdatedAt);
|
||||
expect(workflow.settings.approvalPolicy).toBeUndefined();
|
||||
}
|
||||
|
||||
const magentic = workspace.patterns.find((pattern) => pattern.mode === 'magentic');
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
resolvePatternAgent,
|
||||
resolvePatternAgents,
|
||||
findWorkspaceAgentUsages,
|
||||
normalizeWorkspaceAgentDefinition,
|
||||
type WorkspaceAgentDefinition,
|
||||
} from '@shared/domain/workspaceAgent';
|
||||
import type { PatternAgentDefinition, PatternDefinition } from '@shared/domain/pattern';
|
||||
|
||||
const TIMESTAMP = '2026-04-01T00:00:00.000Z';
|
||||
|
||||
function makeWorkspaceAgent(overrides: Partial<WorkspaceAgentDefinition> = {}): WorkspaceAgentDefinition {
|
||||
return {
|
||||
id: 'wa-1',
|
||||
name: 'Code Reviewer',
|
||||
description: 'Reviews code for quality',
|
||||
instructions: 'Review all code carefully',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInlineAgent(overrides: Partial<PatternAgentDefinition> = {}): PatternAgentDefinition {
|
||||
return {
|
||||
id: 'agent-1',
|
||||
name: 'Inline Agent',
|
||||
description: 'An inline agent',
|
||||
instructions: 'Do stuff',
|
||||
model: 'claude-sonnet-4',
|
||||
reasoningEffort: 'medium',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeLinkedAgent(overrides: Partial<PatternAgentDefinition> = {}): PatternAgentDefinition {
|
||||
return {
|
||||
id: 'agent-linked',
|
||||
name: 'Code Reviewer',
|
||||
description: 'Reviews code for quality',
|
||||
instructions: 'Review all code carefully',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
workspaceAgentId: 'wa-1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makePattern(agents: PatternAgentDefinition[], overrides: Partial<PatternDefinition> = {}): PatternDefinition {
|
||||
return {
|
||||
id: 'pattern-1',
|
||||
name: 'Test Pattern',
|
||||
description: '',
|
||||
mode: 'sequential',
|
||||
availability: 'available',
|
||||
maxIterations: 10,
|
||||
agents,
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolvePatternAgent', () => {
|
||||
const workspaceAgents = [makeWorkspaceAgent()];
|
||||
|
||||
test('returns inline agent unchanged', () => {
|
||||
const agent = makeInlineAgent();
|
||||
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
||||
expect(resolved).toEqual(agent);
|
||||
});
|
||||
|
||||
test('resolves linked agent from workspace agent base', () => {
|
||||
const agent = makeLinkedAgent();
|
||||
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
||||
expect(resolved.name).toBe('Code Reviewer');
|
||||
expect(resolved.model).toBe('gpt-5.4');
|
||||
expect(resolved.instructions).toBe('Review all code carefully');
|
||||
expect(resolved.workspaceAgentId).toBe('wa-1');
|
||||
expect(resolved.id).toBe('agent-linked');
|
||||
});
|
||||
|
||||
test('applies per-pattern overrides on top of workspace agent', () => {
|
||||
const agent = makeLinkedAgent({
|
||||
overrides: { model: 'claude-opus-4', instructions: 'Override instructions' },
|
||||
});
|
||||
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
||||
expect(resolved.model).toBe('claude-opus-4');
|
||||
expect(resolved.instructions).toBe('Override instructions');
|
||||
expect(resolved.name).toBe('Code Reviewer');
|
||||
expect(resolved.description).toBe('Reviews code for quality');
|
||||
});
|
||||
|
||||
test('falls back to inline fields when workspace agent is missing', () => {
|
||||
const agent = makeLinkedAgent({ workspaceAgentId: 'nonexistent' });
|
||||
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
||||
expect(resolved).toEqual(agent);
|
||||
});
|
||||
|
||||
test('partial overrides only replace specified fields', () => {
|
||||
const agent = makeLinkedAgent({
|
||||
overrides: { name: 'Custom Name' },
|
||||
});
|
||||
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
||||
expect(resolved.name).toBe('Custom Name');
|
||||
expect(resolved.model).toBe('gpt-5.4');
|
||||
expect(resolved.reasoningEffort).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePatternAgents', () => {
|
||||
const workspaceAgents = [makeWorkspaceAgent()];
|
||||
|
||||
test('resolves all agents in a pattern', () => {
|
||||
const pattern = makePattern([
|
||||
makeInlineAgent(),
|
||||
makeLinkedAgent(),
|
||||
]);
|
||||
const resolved = resolvePatternAgents(pattern, workspaceAgents);
|
||||
expect(resolved.agents[0].name).toBe('Inline Agent');
|
||||
expect(resolved.agents[1].name).toBe('Code Reviewer');
|
||||
expect(resolved.agents[1].workspaceAgentId).toBe('wa-1');
|
||||
});
|
||||
|
||||
test('preserves pattern metadata', () => {
|
||||
const pattern = makePattern([makeInlineAgent()], { id: 'p-custom', name: 'Custom' });
|
||||
const resolved = resolvePatternAgents(pattern, workspaceAgents);
|
||||
expect(resolved.id).toBe('p-custom');
|
||||
expect(resolved.name).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findWorkspaceAgentUsages', () => {
|
||||
test('finds patterns referencing a workspace agent', () => {
|
||||
const patterns = [
|
||||
makePattern([makeLinkedAgent()], { id: 'p1', name: 'Pattern 1' }),
|
||||
makePattern([makeInlineAgent()], { id: 'p2', name: 'Pattern 2' }),
|
||||
makePattern(
|
||||
[makeInlineAgent(), makeLinkedAgent({ id: 'agent-linked-2' })],
|
||||
{ id: 'p3', name: 'Pattern 3' },
|
||||
),
|
||||
];
|
||||
const usages = findWorkspaceAgentUsages('wa-1', patterns);
|
||||
expect(usages).toHaveLength(2);
|
||||
expect(usages[0].patternId).toBe('p1');
|
||||
expect(usages[1].patternId).toBe('p3');
|
||||
});
|
||||
|
||||
test('returns empty when no patterns reference the agent', () => {
|
||||
const patterns = [makePattern([makeInlineAgent()])];
|
||||
const usages = findWorkspaceAgentUsages('wa-1', patterns);
|
||||
expect(usages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeWorkspaceAgentDefinition', () => {
|
||||
test('trims string fields', () => {
|
||||
const agent = makeWorkspaceAgent({
|
||||
name: ' Code Reviewer ',
|
||||
description: ' Reviews code ',
|
||||
instructions: ' Review carefully ',
|
||||
model: ' gpt-5.4 ',
|
||||
});
|
||||
const normalized = normalizeWorkspaceAgentDefinition(agent);
|
||||
expect(normalized.name).toBe('Code Reviewer');
|
||||
expect(normalized.description).toBe('Reviews code');
|
||||
expect(normalized.instructions).toBe('Review carefully');
|
||||
expect(normalized.model).toBe('gpt-5.4');
|
||||
});
|
||||
|
||||
test('preserves non-string fields', () => {
|
||||
const agent = makeWorkspaceAgent({ reasoningEffort: 'xhigh' });
|
||||
const normalized = normalizeWorkspaceAgentDefinition(agent);
|
||||
expect(normalized.reasoningEffort).toBe('xhigh');
|
||||
expect(normalized.id).toBe('wa-1');
|
||||
expect(normalized.createdAt).toBe(TIMESTAMP);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user