Files
aryx/tests/main/appServiceScratchpadDirectories.test.ts
T
David KayaandCopilot 805e369b67 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>
2026-04-06 22:37:00 +02:00

264 lines
8.6 KiB
TypeScript

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 { 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';
const TIMESTAMP = '2026-03-28T00:00:00.000Z';
const USER_DATA_PATH = 'C:\\workspace\\personal\\repositories\\aryx\\tests\\fixtures';
mock.module('electron', () => {
const electronMock = {
app: {
isPackaged: false,
getAppPath: () => 'C:\\workspace\\personal\\repositories\\aryx',
getPath: () => USER_DATA_PATH,
},
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');
async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
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.');
}
return pattern;
}
function createScratchpadSession(workflowId: string, overrides?: Partial<SessionRecord>): SessionRecord {
return {
id: 'session-scratchpad',
projectId: 'project-scratchpad',
workflowId,
title: 'Scratchpad',
createdAt: TIMESTAMP,
updatedAt: TIMESTAMP,
status: 'idle',
messages: [],
runs: [],
...overrides,
};
}
function createService(
workspace: WorkspaceState,
options?: {
onDeleteSession?: (sessionId: string) => Promise<void>;
},
): 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.loadAvailableModelCatalog = async () => [];
(
service as unknown as {
sidecar: {
deleteSession: (sessionId: string) => Promise<void>;
};
}
).sidecar = {
deleteSession: async (sessionId: string) => {
await options?.onDeleteSession?.(sessionId);
},
};
return service;
}
beforeEach(async () => {
await rm(join(USER_DATA_PATH, 'scratchpad'), { recursive: true, force: true });
});
afterEach(async () => {
await rm(join(USER_DATA_PATH, 'scratchpad'), { recursive: true, force: true });
});
describe('AryxAppService scratchpad directories', () => {
test('creates a dedicated directory for new scratchpad sessions', async () => {
const workspace = createWorkspaceSeed();
const pattern = requireSinglePattern(workspace);
const scratchpadProject = createScratchpadProject(join(USER_DATA_PATH, 'scratchpad'), TIMESTAMP);
workspace.projects = [scratchpadProject];
workspace.selectedProjectId = scratchpadProject.id;
workspace.selectedWorkflowId = pattern.id;
const service = createService(workspace);
const result = await service.createSession(scratchpadProject.id, pattern.id);
const session = result.sessions[0];
if (!session) {
throw new Error('Expected createSession to prepend a scratchpad session.');
}
expect(session.cwd).toBe(join(USER_DATA_PATH, 'scratchpad', session.id));
expect(await pathExists(session.cwd!)).toBe(true);
});
test('creates a new directory when duplicating a scratchpad session', async () => {
const workspace = createWorkspaceSeed();
const pattern = requireSinglePattern(workspace);
const scratchpadProject = createScratchpadProject(join(USER_DATA_PATH, 'scratchpad'), TIMESTAMP);
const originalDirectory = join(USER_DATA_PATH, 'scratchpad', 'session-original');
const originalSession = createScratchpadSession(pattern.id, {
id: 'session-original',
cwd: originalDirectory,
messages: [
{
id: 'msg-1',
role: 'user',
authorName: 'You',
content: 'Draft a plan.',
createdAt: TIMESTAMP,
},
],
});
workspace.projects = [scratchpadProject];
workspace.sessions = [originalSession];
workspace.selectedProjectId = scratchpadProject.id;
workspace.selectedWorkflowId = pattern.id;
workspace.selectedSessionId = originalSession.id;
const service = createService(workspace);
const result = await service.duplicateSession(originalSession.id);
const duplicate = result.sessions[0];
if (!duplicate) {
throw new Error('Expected duplicateSession to prepend the duplicate.');
}
expect(duplicate.id).not.toBe(originalSession.id);
expect(duplicate.cwd).toBe(join(USER_DATA_PATH, 'scratchpad', duplicate.id));
expect(duplicate.cwd).not.toBe(originalSession.cwd);
expect(await pathExists(duplicate.cwd!)).toBe(true);
});
test('creates a new directory when branching a scratchpad session', async () => {
const workspace = createWorkspaceSeed();
const pattern = requireSinglePattern(workspace);
const scratchpadProject = createScratchpadProject(join(USER_DATA_PATH, 'scratchpad'), TIMESTAMP);
const originalDirectory = join(USER_DATA_PATH, 'scratchpad', 'session-original');
const originalSession = createScratchpadSession(pattern.id, {
id: 'session-original',
cwd: originalDirectory,
messages: [
{
id: 'msg-1',
role: 'user',
authorName: 'You',
content: 'Draft a plan.',
createdAt: TIMESTAMP,
},
{
id: 'msg-2',
role: 'assistant',
authorName: 'Primary Agent',
content: 'Here is the first approach.',
createdAt: TIMESTAMP,
},
{
id: 'msg-3',
role: 'user',
authorName: 'You',
content: 'Try again from here with a stricter checklist.',
createdAt: TIMESTAMP,
},
{
id: 'msg-4',
role: 'assistant',
authorName: 'Primary Agent',
content: 'Here is the revised approach.',
createdAt: TIMESTAMP,
},
],
});
workspace.projects = [scratchpadProject];
workspace.sessions = [originalSession];
workspace.selectedProjectId = scratchpadProject.id;
workspace.selectedWorkflowId = pattern.id;
workspace.selectedSessionId = originalSession.id;
const service = createService(workspace);
const result = await service.branchSession(originalSession.id, 'msg-3');
const branch = result.sessions[0];
if (!branch) {
throw new Error('Expected branchSession to prepend the branch.');
}
expect(branch.id).not.toBe(originalSession.id);
expect(branch.cwd).toBe(join(USER_DATA_PATH, 'scratchpad', branch.id));
expect(branch.cwd).not.toBe(originalSession.cwd);
expect(branch.branchOrigin).toMatchObject({
sourceSessionId: originalSession.id,
sourceMessageId: 'msg-3',
sourceMessageIndex: 2,
});
expect(branch.messages.map((message) => message.id)).toEqual(['msg-1', 'msg-2', 'msg-3']);
expect(result.selectedSessionId).toBe(branch.id);
expect(await pathExists(branch.cwd!)).toBe(true);
});
test('removes the scratchpad directory when deleting a session', async () => {
const workspace = createWorkspaceSeed();
const pattern = requireSinglePattern(workspace);
const scratchpadProject = createScratchpadProject(join(USER_DATA_PATH, 'scratchpad'), TIMESTAMP);
const sessionDirectory = join(USER_DATA_PATH, 'scratchpad', 'session-scratchpad');
const session = createScratchpadSession(pattern.id, {
cwd: sessionDirectory,
});
workspace.projects = [scratchpadProject];
workspace.sessions = [session];
workspace.selectedProjectId = scratchpadProject.id;
workspace.selectedWorkflowId = pattern.id;
workspace.selectedSessionId = session.id;
const service = createService(workspace);
await mkdir(sessionDirectory, { recursive: true });
expect(await pathExists(sessionDirectory)).toBe(true);
const result = await service.deleteSession(session.id);
expect(result.sessions).toHaveLength(0);
expect(await pathExists(sessionDirectory)).toBe(false);
});
});