mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-25 20:33:58 +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:
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { RunTurnCommand } from '@shared/contracts/sidecar';
|
||||
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 { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
@@ -49,11 +49,11 @@ function createProject(overrides?: Partial<ProjectRecord>): ProjectRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(projectId: string, patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(projectId: string, workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-alpha',
|
||||
projectId,
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Alpha session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -66,7 +66,7 @@ function createSession(projectId: string, patternId: string, overrides?: Partial
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
pattern: PatternDefinition,
|
||||
pattern: WorkflowDefinition,
|
||||
options?: {
|
||||
captureRunTurn?: (command: RunTurnCommand) => void;
|
||||
},
|
||||
@@ -107,7 +107,7 @@ function createService(
|
||||
describe('AryxAppService discovered tooling', () => {
|
||||
test('allows project-discovered MCP servers to be accepted and used in project sessions', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
}
|
||||
@@ -139,7 +139,7 @@ describe('AryxAppService discovered tooling', () => {
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
@@ -168,7 +168,7 @@ describe('AryxAppService discovered tooling', () => {
|
||||
|
||||
test('allows accepted user-discovered MCP servers to be used across projects', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
}
|
||||
@@ -199,7 +199,7 @@ describe('AryxAppService discovered tooling', () => {
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
@@ -234,7 +234,7 @@ describe('AryxAppService discovered tooling', () => {
|
||||
|
||||
test('selecting a session also selects that session project', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { RunTurnCommand } from '@shared/contracts/sidecar';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import type {
|
||||
ProjectGitRunChangeSummary,
|
||||
ProjectGitWorkingTreeSnapshot,
|
||||
@@ -68,11 +68,11 @@ function createProject(overrides?: Partial<ProjectRecord>): ProjectRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(projectId: string, patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(projectId: string, workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-alpha',
|
||||
projectId,
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Alpha session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -88,12 +88,12 @@ function createFixture(overrides?: {
|
||||
session?: Partial<SessionRecord>;
|
||||
}): {
|
||||
workspace: WorkspaceState;
|
||||
pattern: PatternDefinition;
|
||||
pattern: WorkflowDefinition;
|
||||
project: ProjectRecord;
|
||||
session: SessionRecord;
|
||||
} {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent pattern.');
|
||||
}
|
||||
@@ -104,7 +104,7 @@ function createFixture(overrides?: {
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
return { workspace, pattern, project, session };
|
||||
@@ -173,7 +173,7 @@ function createRunSummary(): ProjectGitRunChangeSummary {
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
pattern: PatternDefinition,
|
||||
pattern: WorkflowDefinition,
|
||||
options?: {
|
||||
snapshot?: ProjectGitWorkingTreeSnapshot;
|
||||
runSummary?: ProjectGitRunChangeSummary;
|
||||
|
||||
@@ -2,12 +2,12 @@ import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { PendingApprovalRecord } from '@shared/domain/approval';
|
||||
import type { PendingMcpAuthRecord } from '@shared/domain/mcpAuth';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { PendingPlanReviewRecord } from '@shared/domain/planReview';
|
||||
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import type { PendingUserInputRecord } from '@shared/domain/userInput';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
|
||||
const TIMESTAMP = '2026-03-28T00:00:00.000Z';
|
||||
const INTERRUPTED_RUN_ERROR =
|
||||
@@ -44,20 +44,20 @@ mock.module('keytar', () => ({
|
||||
|
||||
const { AryxAppService } = await import('@main/AryxAppService');
|
||||
|
||||
function requireSinglePattern(workspace: WorkspaceState): PatternDefinition {
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent pattern.');
|
||||
function requireSingleWorkflow(workspace: WorkspaceState): WorkflowDefinition {
|
||||
const workflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!workflow) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent workflow.');
|
||||
}
|
||||
|
||||
return pattern;
|
||||
return workflow;
|
||||
}
|
||||
|
||||
function createSession(patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-1',
|
||||
projectId: 'project-1',
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -69,7 +69,7 @@ function createSession(patternId: string, overrides?: Partial<SessionRecord>): S
|
||||
}
|
||||
|
||||
function createRun(
|
||||
pattern: PatternDefinition,
|
||||
workflow: WorkflowDefinition,
|
||||
overrides?: Partial<SessionRunRecord>,
|
||||
): SessionRunRecord {
|
||||
return {
|
||||
@@ -78,9 +78,9 @@ function createRun(
|
||||
projectId: 'project-1',
|
||||
projectPath: 'C:\\workspace\\personal\\repositories\\aryx',
|
||||
workspaceKind: 'project',
|
||||
patternId: pattern.id,
|
||||
patternName: pattern.name,
|
||||
patternMode: pattern.mode,
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name,
|
||||
workflowMode: workflow.settings.orchestrationMode ?? 'single',
|
||||
triggerMessageId: 'message-1',
|
||||
startedAt: TIMESTAMP,
|
||||
status: 'running',
|
||||
@@ -171,9 +171,9 @@ function createService(
|
||||
describe('AryxAppService interrupted session cleanup', () => {
|
||||
test('clears stale approvals and user input, then fails the interrupted run on load', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = requireSinglePattern(workspace);
|
||||
const runningRun = createRun(pattern);
|
||||
const session = createSession(pattern.id, {
|
||||
const workflow = requireSingleWorkflow(workspace);
|
||||
const runningRun = createRun(workflow);
|
||||
const session = createSession(workflow.id, {
|
||||
status: 'running',
|
||||
pendingApproval: createPendingApproval('approval-1', 'Approve reading the repo'),
|
||||
pendingApprovalQueue: [createPendingApproval('approval-2', 'Approve writing the repo')],
|
||||
@@ -212,10 +212,10 @@ describe('AryxAppService interrupted session cleanup', () => {
|
||||
|
||||
test('fails sessions that were left in running state even without pending interaction records', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = requireSinglePattern(workspace);
|
||||
const session = createSession(pattern.id, {
|
||||
const workflow = requireSingleWorkflow(workspace);
|
||||
const session = createSession(workflow.id, {
|
||||
status: 'running',
|
||||
runs: [createRun(pattern)],
|
||||
runs: [createRun(workflow)],
|
||||
});
|
||||
workspace.sessions = [session];
|
||||
|
||||
@@ -235,10 +235,10 @@ describe('AryxAppService interrupted session cleanup', () => {
|
||||
|
||||
test('preserves restart-safe plan review and MCP auth prompts', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = requireSinglePattern(workspace);
|
||||
const workflow = requireSingleWorkflow(workspace);
|
||||
const planReview = createPendingPlanReview();
|
||||
const mcpAuth = createPendingMcpAuth();
|
||||
const session = createSession(pattern.id, {
|
||||
const session = createSession(workflow.id, {
|
||||
pendingPlanReview: planReview,
|
||||
pendingMcpAuth: mcpAuth,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { RunTurnCommand } from '@shared/contracts/sidecar';
|
||||
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 { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
@@ -49,11 +49,11 @@ function createProject(overrides?: Partial<ProjectRecord>): ProjectRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(projectId: string, patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(projectId: string, workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-alpha',
|
||||
projectId,
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Alpha session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -102,12 +102,12 @@ function createSession(projectId: string, patternId: string, overrides?: Partial
|
||||
|
||||
function createFixture(): {
|
||||
workspace: WorkspaceState;
|
||||
pattern: PatternDefinition;
|
||||
pattern: WorkflowDefinition;
|
||||
project: ProjectRecord;
|
||||
session: SessionRecord;
|
||||
} {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent pattern.');
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function createFixture(): {
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
return { workspace, pattern, project, session };
|
||||
@@ -126,7 +126,7 @@ function createFixture(): {
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
pattern: PatternDefinition,
|
||||
pattern: WorkflowDefinition,
|
||||
options?: {
|
||||
captureRunTurn?: (command: RunTurnCommand) => void;
|
||||
},
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import { resolvePatternGraph } from '@shared/domain/pattern';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
const TIMESTAMP = '2026-03-27T00:00:00.000Z';
|
||||
|
||||
mock.module('electron', () => {
|
||||
const electronMock = {
|
||||
app: {
|
||||
isPackaged: false,
|
||||
getAppPath: () => 'C:\\workspace\\personal\\repositories\\aryx',
|
||||
getPath: () => 'C:\\workspace\\personal\\repositories\\aryx\\tests\\fixtures',
|
||||
},
|
||||
dialog: {
|
||||
showOpenDialog: async () => ({ canceled: true, filePaths: [] }),
|
||||
},
|
||||
shell: {
|
||||
openPath: async () => '',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...electronMock,
|
||||
default: electronMock,
|
||||
};
|
||||
});
|
||||
|
||||
mock.module('keytar', () => ({
|
||||
default: {
|
||||
getPassword: async () => null,
|
||||
setPassword: async () => undefined,
|
||||
deletePassword: async () => false,
|
||||
},
|
||||
}));
|
||||
|
||||
const { AryxAppService } = await import('@main/AryxAppService');
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
options?: { knownApprovalToolNames?: string[] },
|
||||
): InstanceType<typeof AryxAppService> {
|
||||
const service = new AryxAppService();
|
||||
const internals = service as unknown as Record<string, unknown>;
|
||||
internals.loadWorkspace = async () => workspace;
|
||||
internals.persistAndBroadcast = async (nextWorkspace: WorkspaceState) => nextWorkspace;
|
||||
internals.listKnownApprovalToolNames = async () => options?.knownApprovalToolNames ?? ['read', 'write', 'shell'];
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function requirePattern(workspace: WorkspaceState, mode: PatternDefinition['mode']): PatternDefinition {
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === mode);
|
||||
if (!pattern) {
|
||||
throw new Error(`Expected workspace seed to include a ${mode} pattern.`);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
describe('AryxAppService deletePattern', () => {
|
||||
test('deletes a built-in pattern and tracks its ID', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const builtinPattern = requirePattern(workspace, 'sequential');
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.deletePattern(builtinPattern.id);
|
||||
|
||||
expect(result.patterns.find((p) => p.id === builtinPattern.id)).toBeUndefined();
|
||||
expect(result.deletedBuiltinPatternIds).toContain(builtinPattern.id);
|
||||
});
|
||||
|
||||
test('deletes a custom pattern without tracking its ID', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const customPattern: PatternDefinition = {
|
||||
...requirePattern(workspace, 'single'),
|
||||
id: 'custom-pattern',
|
||||
name: 'My Custom Pattern',
|
||||
};
|
||||
workspace.patterns.push(customPattern);
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.deletePattern('custom-pattern');
|
||||
|
||||
expect(result.patterns.find((p) => p.id === 'custom-pattern')).toBeUndefined();
|
||||
expect(result.deletedBuiltinPatternIds ?? []).not.toContain('custom-pattern');
|
||||
});
|
||||
|
||||
test('selects first remaining pattern when the active pattern is deleted', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const targetPattern = requirePattern(workspace, 'single');
|
||||
workspace.selectedPatternId = targetPattern.id;
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.deletePattern(targetPattern.id);
|
||||
|
||||
expect(result.selectedPatternId).toBe(result.patterns[0]?.id);
|
||||
expect(result.selectedPatternId).not.toBe(targetPattern.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AryxAppService savePattern', () => {
|
||||
test('preserves a provided custom graph instead of re-syncing it', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = requirePattern(workspace, 'sequential');
|
||||
const baseGraph = resolvePatternGraph(pattern);
|
||||
const customGraph = {
|
||||
...baseGraph,
|
||||
nodes: baseGraph.nodes.map((node, index) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: node.position.x + 37 + index,
|
||||
y: node.position.y + 19 + index * 7,
|
||||
},
|
||||
})),
|
||||
};
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.savePattern({
|
||||
...pattern,
|
||||
graph: customGraph,
|
||||
updatedAt: TIMESTAMP,
|
||||
});
|
||||
|
||||
const savedPattern = requirePattern(result, 'sequential');
|
||||
expect(savedPattern.graph).toEqual(customGraph);
|
||||
});
|
||||
|
||||
test('still seeds a default graph when the pattern graph is missing', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = requirePattern(workspace, 'group-chat');
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.savePattern({
|
||||
...pattern,
|
||||
graph: undefined,
|
||||
updatedAt: TIMESTAMP,
|
||||
});
|
||||
|
||||
const savedPattern = requirePattern(result, 'group-chat');
|
||||
expect(savedPattern.graph).toEqual(resolvePatternGraph(pattern));
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,10 @@ import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { RunTurnCommand } from '@shared/contracts/sidecar';
|
||||
import { buildAvailableModelCatalog } from '@shared/domain/models';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
import { resolveWorkflowAgentNodes, type WorkflowDefinition } from '@shared/domain/workflow';
|
||||
|
||||
const TIMESTAMP = '2026-03-28T00:00:00.000Z';
|
||||
|
||||
@@ -50,11 +50,11 @@ function createProject(overrides?: Partial<ProjectRecord>): ProjectRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(projectId: string, patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(projectId: string, workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-alpha',
|
||||
projectId,
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Alpha session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -67,7 +67,7 @@ function createSession(projectId: string, patternId: string, overrides?: Partial
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
pattern: PatternDefinition,
|
||||
workflow: WorkflowDefinition,
|
||||
options?: {
|
||||
captureRunTurn?: (command: RunTurnCommand) => void;
|
||||
},
|
||||
@@ -76,7 +76,7 @@ function createService(
|
||||
const internals = service as unknown as Record<string, unknown>;
|
||||
internals.loadWorkspace = async () => workspace;
|
||||
internals.persistAndBroadcast = async (nextWorkspace: WorkspaceState) => nextWorkspace;
|
||||
internals.buildEffectivePattern = async () => pattern;
|
||||
internals.buildEffectiveWorkflow = async () => workflow;
|
||||
internals.loadAvailableModelCatalog = async () => buildAvailableModelCatalog();
|
||||
internals.awaitFinalResponseApproval = async () => undefined;
|
||||
internals.finalizeTurn = () => undefined;
|
||||
@@ -101,19 +101,45 @@ function createService(
|
||||
return service;
|
||||
}
|
||||
|
||||
function updatePrimaryAgent(
|
||||
workflow: WorkflowDefinition,
|
||||
update: (agent: NonNullable<ReturnType<typeof resolveWorkflowAgentNodes>[number]>['config']) => NonNullable<ReturnType<typeof resolveWorkflowAgentNodes>[number]>['config'],
|
||||
): WorkflowDefinition {
|
||||
let updated = false;
|
||||
return {
|
||||
...workflow,
|
||||
graph: {
|
||||
...workflow.graph,
|
||||
nodes: workflow.graph.nodes.map((node) => {
|
||||
if (updated || node.kind !== 'agent' || node.config.kind !== 'agent') {
|
||||
return node;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
return {
|
||||
...node,
|
||||
config: update(node.config),
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getPrimaryAgentConfig(workflow: WorkflowDefinition | undefined) {
|
||||
const node = workflow ? resolveWorkflowAgentNodes(workflow)[0] : undefined;
|
||||
return node?.config.kind === 'agent' ? node.config : undefined;
|
||||
}
|
||||
|
||||
describe('AryxAppService project customization', () => {
|
||||
test('sendSessionMessage injects project instructions and enabled project agent profiles', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const basePattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!basePattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
const baseWorkflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!baseWorkflow) {
|
||||
throw new Error('Expected a single-agent workflow in the workspace seed.');
|
||||
}
|
||||
|
||||
const pattern: PatternDefinition = {
|
||||
...basePattern,
|
||||
agents: [
|
||||
{
|
||||
...basePattern.agents[0]!,
|
||||
const workflow = updatePrimaryAgent(baseWorkflow, (agent) => ({
|
||||
...agent,
|
||||
copilot: {
|
||||
customAgents: [
|
||||
{
|
||||
@@ -122,9 +148,7 @@ describe('AryxAppService project customization', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}));
|
||||
|
||||
const project = createProject({
|
||||
customization: {
|
||||
@@ -171,16 +195,16 @@ describe('AryxAppService project customization', () => {
|
||||
lastScannedAt: TIMESTAMP,
|
||||
},
|
||||
});
|
||||
const session = createSession(project.id, pattern.id);
|
||||
const session = createSession(project.id, workflow.id);
|
||||
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = workflow.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
const service = createService(workspace, pattern, {
|
||||
const service = createService(workspace, workflow, {
|
||||
captureRunTurn: (capturedCommand) => {
|
||||
command = capturedCommand;
|
||||
},
|
||||
@@ -189,7 +213,7 @@ describe('AryxAppService project customization', () => {
|
||||
await service.sendSessionMessage(session.id, 'Use the repository guidance.');
|
||||
|
||||
expect(command?.projectInstructions).toBe('Use TypeScript.\n\nPrefer focused tests.');
|
||||
expect(command?.pattern.agents[0]?.copilot?.customAgents).toEqual([
|
||||
expect(getPrimaryAgentConfig(command?.workflow)?.copilot?.customAgents).toEqual([
|
||||
{
|
||||
name: 'reviewer',
|
||||
prompt: 'Built-in reviewer prompt.',
|
||||
@@ -206,9 +230,9 @@ describe('AryxAppService project customization', () => {
|
||||
|
||||
test('sendSessionMessage formats file-scoped and task-scoped project instructions for the sidecar', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
const workflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!workflow) {
|
||||
throw new Error('Expected a single-agent workflow in the workspace seed.');
|
||||
}
|
||||
|
||||
const project = createProject({
|
||||
@@ -248,16 +272,16 @@ describe('AryxAppService project customization', () => {
|
||||
lastScannedAt: TIMESTAMP,
|
||||
},
|
||||
});
|
||||
const session = createSession(project.id, pattern.id);
|
||||
const session = createSession(project.id, workflow.id);
|
||||
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = workflow.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
const service = createService(workspace, pattern, {
|
||||
const service = createService(workspace, workflow, {
|
||||
captureRunTurn: (capturedCommand) => {
|
||||
command = capturedCommand;
|
||||
},
|
||||
@@ -286,22 +310,22 @@ describe('AryxAppService project customization', () => {
|
||||
|
||||
test('sendSessionMessage carries structured prompt invocations and uses prompt agent plan mode', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
const workflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!workflow) {
|
||||
throw new Error('Expected a single-agent workflow in the workspace seed.');
|
||||
}
|
||||
|
||||
const project = createProject();
|
||||
const session = createSession(project.id, pattern.id);
|
||||
const session = createSession(project.id, workflow.id);
|
||||
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = workflow.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
const service = createService(workspace, pattern, {
|
||||
const service = createService(workspace, workflow, {
|
||||
captureRunTurn: (capturedCommand) => {
|
||||
command = capturedCommand;
|
||||
},
|
||||
@@ -348,21 +372,16 @@ describe('AryxAppService project customization', () => {
|
||||
|
||||
test('sendSessionMessage hydrates prompt model metadata and overrides the turn pattern model', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const basePattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!basePattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
const baseWorkflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!baseWorkflow) {
|
||||
throw new Error('Expected a single-agent workflow in the workspace seed.');
|
||||
}
|
||||
|
||||
const pattern: PatternDefinition = {
|
||||
...basePattern,
|
||||
agents: [
|
||||
{
|
||||
...basePattern.agents[0]!,
|
||||
const workflow = updatePrimaryAgent(baseWorkflow, (agent) => ({
|
||||
...agent,
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
],
|
||||
};
|
||||
}));
|
||||
|
||||
const project = createProject({
|
||||
customization: {
|
||||
@@ -382,16 +401,16 @@ describe('AryxAppService project customization', () => {
|
||||
lastScannedAt: TIMESTAMP,
|
||||
},
|
||||
});
|
||||
const session = createSession(project.id, pattern.id);
|
||||
const session = createSession(project.id, workflow.id);
|
||||
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = workflow.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
let command: RunTurnCommand | undefined;
|
||||
const service = createService(workspace, pattern, {
|
||||
const service = createService(workspace, workflow, {
|
||||
captureRunTurn: (capturedCommand) => {
|
||||
command = capturedCommand;
|
||||
},
|
||||
@@ -412,8 +431,8 @@ describe('AryxAppService project customization', () => {
|
||||
model: 'Claude Sonnet 4.5',
|
||||
resolvedPrompt: 'Review the REST API for security gaps.',
|
||||
});
|
||||
expect(command?.pattern.agents[0]?.model).toBe('claude-sonnet-4.5');
|
||||
expect(command?.pattern.agents[0]?.reasoningEffort).toBeUndefined();
|
||||
expect(getPrimaryAgentConfig(command?.workflow)?.model).toBe('claude-sonnet-4.5');
|
||||
expect(getPrimaryAgentConfig(command?.workflow)?.reasoningEffort).toBeUndefined();
|
||||
expect(command?.promptInvocation).toEqual({
|
||||
id: 'project_customization_prompt_rest_review',
|
||||
name: 'rest-review',
|
||||
@@ -426,9 +445,9 @@ describe('AryxAppService project customization', () => {
|
||||
|
||||
test('setProjectAgentProfileEnabled persists the updated enabled state', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a single-agent pattern in the workspace seed.');
|
||||
const workflow = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!workflow) {
|
||||
throw new Error('Expected a single-agent workflow in the workspace seed.');
|
||||
}
|
||||
|
||||
const project = createProject({
|
||||
@@ -447,12 +466,12 @@ describe('AryxAppService project customization', () => {
|
||||
lastScannedAt: TIMESTAMP,
|
||||
},
|
||||
});
|
||||
const session = createSession(project.id, pattern.id);
|
||||
const session = createSession(project.id, workflow.id);
|
||||
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
|
||||
const service = createService(workspace, pattern);
|
||||
const service = createService(workspace, workflow);
|
||||
const updated = await service.setProjectAgentProfileEnabled(project.id, 'agent-readme', false);
|
||||
|
||||
expect(updated.projects[0]?.customization?.agentProfiles).toEqual([
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
import { access, mkdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import { createScratchpadProject } from '@shared/domain/project';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
@@ -50,8 +50,8 @@ async function pathExists(path: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function requireSinglePattern(workspace: WorkspaceState): PatternDefinition {
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
function requireSinglePattern(workspace: WorkspaceState): WorkflowDefinition {
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent pattern.');
|
||||
}
|
||||
@@ -59,11 +59,11 @@ function requireSinglePattern(workspace: WorkspaceState): PatternDefinition {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
function createScratchpadSession(patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createScratchpadSession(workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-scratchpad',
|
||||
projectId: 'project-scratchpad',
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Scratchpad',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -116,7 +116,7 @@ describe('AryxAppService scratchpad directories', () => {
|
||||
const scratchpadProject = createScratchpadProject(join(USER_DATA_PATH, 'scratchpad'), TIMESTAMP);
|
||||
workspace.projects = [scratchpadProject];
|
||||
workspace.selectedProjectId = scratchpadProject.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
|
||||
const service = createService(workspace);
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('AryxAppService scratchpad directories', () => {
|
||||
workspace.projects = [scratchpadProject];
|
||||
workspace.sessions = [originalSession];
|
||||
workspace.selectedProjectId = scratchpadProject.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = originalSession.id;
|
||||
|
||||
const service = createService(workspace);
|
||||
@@ -212,7 +212,7 @@ describe('AryxAppService scratchpad directories', () => {
|
||||
workspace.projects = [scratchpadProject];
|
||||
workspace.sessions = [originalSession];
|
||||
workspace.selectedProjectId = scratchpadProject.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = originalSession.id;
|
||||
|
||||
const service = createService(workspace);
|
||||
@@ -248,7 +248,7 @@ describe('AryxAppService scratchpad directories', () => {
|
||||
workspace.projects = [scratchpadProject];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = scratchpadProject.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
|
||||
const service = createService(workspace);
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('AryxAppService sub-workflow operations', () => {
|
||||
expect(session?.workflowId).toBe('parent');
|
||||
expect(session?.sessionModelConfig).toEqual({
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'medium',
|
||||
reasoningEffort: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,11 +48,11 @@ function createProject(overrides?: Partial<ProjectRecord>): ProjectRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createSession(patternId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
function createSession(workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
|
||||
return {
|
||||
id: 'session-1',
|
||||
projectId: 'project-1',
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Terminal Session',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -133,7 +133,7 @@ function createService(workspace: WorkspaceState, terminalSnapshot = createTermi
|
||||
describe('AryxAppService terminal integration', () => {
|
||||
test('uses the selected session cwd when creating and restarting the terminal', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns[0];
|
||||
const pattern = workspace.workflows[0];
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a seeded pattern.');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { RunTurnCommand } from '@shared/contracts/sidecar';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
import { createScratchpadProject } from '@shared/domain/project';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
@@ -43,11 +43,11 @@ const { AryxAppService } = await import('@main/AryxAppService');
|
||||
|
||||
function createWorkspaceFixture(): {
|
||||
workspace: WorkspaceState;
|
||||
pattern: PatternDefinition;
|
||||
pattern: WorkflowDefinition;
|
||||
session: SessionRecord;
|
||||
} {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
|
||||
const pattern = workspace.workflows.find((candidate) => candidate.settings.orchestrationMode === 'single');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected the workspace seed to include a single-agent pattern.');
|
||||
}
|
||||
@@ -56,7 +56,7 @@ function createWorkspaceFixture(): {
|
||||
const session: SessionRecord = {
|
||||
id: 'session-scratchpad',
|
||||
projectId: project.id,
|
||||
patternId: pattern.id,
|
||||
workflowId: pattern.id,
|
||||
title: 'Scratchpad',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -69,7 +69,7 @@ function createWorkspaceFixture(): {
|
||||
workspace.projects = [project];
|
||||
workspace.sessions = [session];
|
||||
workspace.selectedProjectId = project.id;
|
||||
workspace.selectedPatternId = pattern.id;
|
||||
workspace.selectedWorkflowId = pattern.id;
|
||||
workspace.selectedSessionId = session.id;
|
||||
workspace.settings.tooling = {
|
||||
mcpServers: [
|
||||
@@ -105,7 +105,7 @@ function createWorkspaceFixture(): {
|
||||
|
||||
function createService(
|
||||
workspace: WorkspaceState,
|
||||
pattern: PatternDefinition,
|
||||
pattern: WorkflowDefinition,
|
||||
options?: {
|
||||
captureRunTurn?: (command: RunTurnCommand) => void;
|
||||
knownApprovalToolNames?: string[];
|
||||
|
||||
@@ -87,11 +87,13 @@ function createWorkflow(): WorkflowDefinition {
|
||||
describe('AryxAppService workflow operations', () => {
|
||||
test('saves workflows into workspace state', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const initialWorkflowCount = workspace.workflows.length;
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.saveWorkflow(createWorkflow());
|
||||
|
||||
expect(result.workflows).toHaveLength(1);
|
||||
expect(result.workflows).toHaveLength(initialWorkflowCount + 1);
|
||||
expect(result.workflows.some((workflow) => workflow.id === 'workflow-test')).toBe(true);
|
||||
expect(result.selectedWorkflowId).toBe('workflow-test');
|
||||
});
|
||||
|
||||
@@ -115,40 +117,28 @@ describe('AryxAppService workflow operations', () => {
|
||||
|
||||
test('creates workflows from templates and selects the new workflow', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const initialWorkflowCount = workspace.workflows.length;
|
||||
const service = createService(workspace);
|
||||
|
||||
const result = await service.createWorkflowFromTemplate('workflow-template-code-review', {
|
||||
name: 'Template Copy',
|
||||
});
|
||||
|
||||
expect(result.workflows).toHaveLength(1);
|
||||
expect(result.workflows[0]?.name).toBe('Template Copy');
|
||||
expect(result.selectedWorkflowId).toBe(result.workflows[0]?.id);
|
||||
});
|
||||
|
||||
test('upgrades a pattern to a saved workflow without removing the pattern', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'handoff');
|
||||
if (!pattern) {
|
||||
throw new Error('Expected a handoff pattern.');
|
||||
}
|
||||
|
||||
const service = createService(workspace);
|
||||
const result = await service.upgradePatternToWorkflow(pattern.id, { save: true });
|
||||
|
||||
expect(result.workspace?.workflows).toHaveLength(1);
|
||||
expect(result.workspace?.patterns.some((candidate) => candidate.id === pattern.id)).toBe(true);
|
||||
expect(result.workflow.id).toBe('workflow-handoff-support');
|
||||
expect(result.workflows).toHaveLength(initialWorkflowCount + 1);
|
||||
const createdWorkflow = result.workflows.find((workflow) => workflow.name === 'Template Copy');
|
||||
expect(createdWorkflow).toBeDefined();
|
||||
expect(result.selectedWorkflowId).toBe(createdWorkflow?.id);
|
||||
});
|
||||
|
||||
test('imports and saves workflows from yaml', async () => {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const initialWorkflowCount = workspace.workflows.length;
|
||||
const service = createService(workspace);
|
||||
const yaml = exportWorkflowDefinition(createWorkflow(), 'yaml').content;
|
||||
|
||||
const result = await service.importWorkflow(yaml, 'yaml', { save: true });
|
||||
|
||||
expect(result.workspace?.workflows).toHaveLength(1);
|
||||
expect(result.workspace?.workflows).toHaveLength(initialWorkflowCount + 1);
|
||||
expect(result.workspace?.selectedWorkflowId).toBe('workflow-test');
|
||||
expect(result.workflow.id).toBe('workflow-test');
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type RunTimelineEventRecord,
|
||||
type SessionRunRecord,
|
||||
} from '@shared/domain/runTimeline';
|
||||
import type { WorkflowDefinition } from '@shared/domain/workflow';
|
||||
|
||||
mock.module('electron', () => {
|
||||
const electronMock = {
|
||||
@@ -200,7 +201,7 @@ describe('AryxAppService workflow checkpointing', () => {
|
||||
workspaceKind: 'scratchpad',
|
||||
mode: 'interactive',
|
||||
messageMode: 'enqueue',
|
||||
pattern: createPattern(),
|
||||
workflow: createWorkflow(),
|
||||
messages: session.messages,
|
||||
resumeFromCheckpoint,
|
||||
}),
|
||||
@@ -230,7 +231,7 @@ describe('AryxAppService workflow checkpointing', () => {
|
||||
});
|
||||
|
||||
function createRunningSession(): { session: SessionRecord; run: SessionRunRecord } {
|
||||
const pattern = createPattern();
|
||||
const workflow = createWorkflow();
|
||||
const run = createSessionRunRecord({
|
||||
requestId: 'turn-1',
|
||||
project: {
|
||||
@@ -239,14 +240,14 @@ function createRunningSession(): { session: SessionRecord; run: SessionRunRecord
|
||||
},
|
||||
workingDirectory: 'C:\\scratchpad',
|
||||
workspaceKind: 'scratchpad',
|
||||
pattern,
|
||||
workflow,
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-04-01T12:00:00.000Z',
|
||||
});
|
||||
const session: SessionRecord = {
|
||||
id: 'session-1',
|
||||
projectId: SCRATCHPAD_PROJECT_ID,
|
||||
patternId: pattern.id,
|
||||
workflowId: workflow.id,
|
||||
title: 'Checkpoint session',
|
||||
createdAt: '2026-04-01T12:00:00.000Z',
|
||||
updatedAt: '2026-04-01T12:00:00.000Z',
|
||||
@@ -274,23 +275,42 @@ function createRunningSession(): { session: SessionRecord; run: SessionRunRecord
|
||||
return { session, run };
|
||||
}
|
||||
|
||||
function createPattern() {
|
||||
function createWorkflow(): WorkflowDefinition {
|
||||
return {
|
||||
id: 'pattern-handoff',
|
||||
id: 'workflow-handoff',
|
||||
name: 'Checkpointing flow',
|
||||
description: '',
|
||||
mode: 'handoff' as const,
|
||||
availability: 'available' as const,
|
||||
maxIterations: 4,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-1',
|
||||
kind: 'agent',
|
||||
label: 'Primary',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
},
|
||||
{ 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: true },
|
||||
executionMode: 'off-thread' as const,
|
||||
orchestrationMode: 'handoff' as const,
|
||||
maxIterations: 4,
|
||||
},
|
||||
createdAt: '2026-04-01T00:00:00.000Z',
|
||||
updatedAt: '2026-04-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
@@ -157,22 +157,41 @@ describe('SidecarClient', () => {
|
||||
requestId: 'turn-1',
|
||||
sessionId: 'session-1',
|
||||
projectPath: 'C:\\workspace\\project',
|
||||
pattern: {
|
||||
id: 'pattern-1',
|
||||
workflow: {
|
||||
id: 'workflow-1',
|
||||
name: 'Single Agent',
|
||||
description: '',
|
||||
mode: 'single',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-1',
|
||||
kind: 'agent',
|
||||
label: 'Primary',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
},
|
||||
{ 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-04-01T00:00:00.000Z',
|
||||
updatedAt: '2026-04-01T00:00:00.000Z',
|
||||
},
|
||||
@@ -253,22 +272,41 @@ describe('SidecarClient', () => {
|
||||
requestId: 'turn-1',
|
||||
sessionId: 'session-1',
|
||||
projectPath: 'C:\\workspace\\project',
|
||||
pattern: {
|
||||
id: 'pattern-1',
|
||||
workflow: {
|
||||
id: 'workflow-1',
|
||||
name: 'Handoff',
|
||||
description: '',
|
||||
mode: 'handoff',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
agents: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
],
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'start', kind: 'start', label: 'Start', position: { x: 0, y: 0 }, config: { kind: 'start' } },
|
||||
{
|
||||
id: 'agent-1',
|
||||
kind: 'agent',
|
||||
label: 'Primary',
|
||||
position: { x: 200, y: 0 },
|
||||
order: 0,
|
||||
config: {
|
||||
kind: 'agent',
|
||||
id: 'agent-1',
|
||||
name: 'Primary',
|
||||
description: '',
|
||||
instructions: 'Help with the request.',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
},
|
||||
{ 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: true },
|
||||
executionMode: 'off-thread',
|
||||
orchestrationMode: 'handoff',
|
||||
maxIterations: 1,
|
||||
},
|
||||
createdAt: '2026-04-01T00:00:00.000Z',
|
||||
updatedAt: '2026-04-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
@@ -27,15 +27,15 @@ const { WorkspaceRepository } = await import('@main/persistence/workspaceReposit
|
||||
function createStoredWorkspace(): WorkspaceState {
|
||||
const workspace = createWorkspaceSeed();
|
||||
const scratchpadProject = createScratchpadProject('C:\\legacy\\scratchpad', TIMESTAMP);
|
||||
const patternId = workspace.patterns[0]?.id;
|
||||
if (!patternId) {
|
||||
const workflowId = workspace.workflows[0]?.id;
|
||||
if (!workflowId) {
|
||||
throw new Error('Expected workspace seed to include at least one pattern.');
|
||||
}
|
||||
|
||||
const session: SessionRecord = {
|
||||
id: 'session-scratchpad',
|
||||
projectId: scratchpadProject.id,
|
||||
patternId,
|
||||
workflowId,
|
||||
title: 'Scratchpad',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
@@ -49,7 +49,7 @@ function createStoredWorkspace(): WorkspaceState {
|
||||
projects: [scratchpadProject],
|
||||
sessions: [session],
|
||||
selectedProjectId: scratchpadProject.id,
|
||||
selectedPatternId: patternId,
|
||||
selectedWorkflowId: workflowId,
|
||||
selectedSessionId: session.id,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user