refactor: extract AryxAppService into focused service delegates

Extract seven responsibility clusters from the 3,466-line AryxAppService
monolith into focused service classes under src/main/services/:

- WorkflowManager: workflow CRUD, templates, validation, resolution
- McpProbeManager: MCP server probing, OAuth pre-auth, probe queuing
- DiscoveredToolingSyncService: tooling/customization scanning, watchers
- GitContextManager: git refresh orchestration, context updates, mutations
- CheckpointRecoveryManager: checkpoint retry/recovery state handling
- ApprovalCoordinator: approval/user-input/plan-review/OAuth state machine
- SessionTurnExecutor: turn execution, streaming deltas, finalization

AryxAppService remains the public facade consumed by IPC handlers but now
delegates internally via constructor-injected service instances. A new
AppServiceDeps type provides a clean dependency injection seam for testing.

The facade is reduced from 3,466 to 2,572 lines. All existing tests pass
with two test files migrated to constructor DI (appServiceGitRefresh,
appServiceMcpProbing). New focused tests added for WorkflowManager and
the DI seam itself.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-07 22:18:52 +02:00
co-authored by Copilot
parent 36b8dd0c12
commit 574455729b
12 changed files with 3230 additions and 1386 deletions
+71
View File
@@ -0,0 +1,71 @@
import { describe, expect, mock, test } from 'bun:test';
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
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');
describe('AryxAppService dependency injection', () => {
test('uses injected sidecar dependencies for capability lookups', async () => {
const capabilities: SidecarCapabilities = {
runtime: 'dotnet-maf',
modes: {
single: { available: true },
sequential: { available: true },
concurrent: { available: true },
handoff: { available: true },
'group-chat': { available: true },
magentic: { available: true },
},
models: [
{
id: 'gpt-5.4',
name: 'GPT-5.4',
},
],
runtimeTools: [],
connection: {
status: 'ready',
summary: 'Ready',
checkedAt: '2026-04-07T00:00:00.000Z',
},
};
const service = new AryxAppService({
sidecar: {
describeCapabilities: async () => capabilities,
dispose: async () => undefined,
} as never,
});
await expect(service.describeSidecarCapabilities()).resolves.toEqual(capabilities);
});
});