mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-28 23:48:39 +02:00
feat: surface workflow diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { WorkflowDiagnosticEvent } 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 workflow diagnostics', () => {
|
||||
test('maps turn-scoped workflow diagnostics to session events', async () => {
|
||||
const service = new AryxAppService();
|
||||
const captured: SessionEventRecord[] = [];
|
||||
const workflowDiagnostic: WorkflowDiagnosticEvent = {
|
||||
type: 'workflow-diagnostic',
|
||||
requestId: 'turn-1',
|
||||
sessionId: 'session-1',
|
||||
severity: 'error',
|
||||
diagnosticKind: 'executor-failed',
|
||||
message: 'Tool crashed.',
|
||||
agentId: 'agent-1',
|
||||
agentName: 'Primary',
|
||||
executorId: 'agent-1',
|
||||
exceptionType: 'InvalidOperationException',
|
||||
};
|
||||
|
||||
const internals = service as unknown as {
|
||||
emitSessionEvent: (event: SessionEventRecord) => void;
|
||||
handleTurnScopedEvent: (
|
||||
workspace: unknown,
|
||||
sessionId: string,
|
||||
event: WorkflowDiagnosticEvent,
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
internals.emitSessionEvent = (event) => {
|
||||
captured.push(event);
|
||||
};
|
||||
|
||||
await internals.handleTurnScopedEvent({}, 'session-1', workflowDiagnostic);
|
||||
|
||||
expect(captured).toHaveLength(1);
|
||||
expect(captured[0]).toMatchObject({
|
||||
sessionId: 'session-1',
|
||||
kind: 'workflow-diagnostic',
|
||||
agentId: 'agent-1',
|
||||
agentName: 'Primary',
|
||||
diagnosticSeverity: 'error',
|
||||
diagnosticKind: 'executor-failed',
|
||||
diagnosticMessage: 'Tool crashed.',
|
||||
executorId: 'agent-1',
|
||||
exceptionType: 'InvalidOperationException',
|
||||
});
|
||||
expect(captured[0]?.occurredAt).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
|
||||
|
||||
import { describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
|
||||
import type { RunTurnCommand, SidecarCapabilities, WorkflowDiagnosticEvent } from '@shared/contracts/sidecar';
|
||||
|
||||
class FakeReadableStream extends EventEmitter {
|
||||
setEncoding(_encoding: BufferEncoding): void {}
|
||||
@@ -142,4 +142,100 @@ describe('SidecarClient', () => {
|
||||
spawnedProcesses[1]!.completeExit();
|
||||
await finalDispose;
|
||||
});
|
||||
|
||||
test('routes workflow diagnostic events through the turn-scoped callback', async () => {
|
||||
spawnedProcesses.length = 0;
|
||||
const client = new SidecarClient();
|
||||
const diagnostics: WorkflowDiagnosticEvent[] = [];
|
||||
const command: RunTurnCommand = {
|
||||
type: 'run-turn',
|
||||
requestId: 'turn-1',
|
||||
sessionId: 'session-1',
|
||||
projectPath: 'C:\\workspace\\project',
|
||||
pattern: {
|
||||
id: 'pattern-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',
|
||||
},
|
||||
],
|
||||
createdAt: '2026-04-01T00:00:00.000Z',
|
||||
updatedAt: '2026-04-01T00:00:00.000Z',
|
||||
},
|
||||
messages: [],
|
||||
};
|
||||
|
||||
const turn = client.runTurn(
|
||||
command,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async () => undefined,
|
||||
async (event) => {
|
||||
if (event.type === 'workflow-diagnostic') {
|
||||
diagnostics.push(event);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(spawnedProcesses).toHaveLength(1);
|
||||
|
||||
spawnedProcesses[0]!.emitStdout(
|
||||
`${JSON.stringify({
|
||||
type: 'workflow-diagnostic',
|
||||
requestId: command.requestId,
|
||||
sessionId: command.sessionId,
|
||||
severity: 'error',
|
||||
diagnosticKind: 'executor-failed',
|
||||
message: 'Tool crashed.',
|
||||
agentId: 'agent-1',
|
||||
agentName: 'Primary',
|
||||
executorId: 'agent-1',
|
||||
exceptionType: 'InvalidOperationException',
|
||||
} satisfies WorkflowDiagnosticEvent)}\n`,
|
||||
);
|
||||
spawnedProcesses[0]!.emitStdout(
|
||||
`${JSON.stringify({
|
||||
type: 'turn-complete',
|
||||
requestId: command.requestId,
|
||||
sessionId: command.sessionId,
|
||||
messages: [],
|
||||
cancelled: false,
|
||||
})}\n`,
|
||||
);
|
||||
spawnedProcesses[0]!.emitStdout(
|
||||
`${JSON.stringify({
|
||||
type: 'command-complete',
|
||||
requestId: command.requestId,
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
await expect(turn).resolves.toEqual([]);
|
||||
expect(diagnostics).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'workflow-diagnostic',
|
||||
severity: 'error',
|
||||
diagnosticKind: 'executor-failed',
|
||||
message: 'Tool crashed.',
|
||||
executorId: 'agent-1',
|
||||
}),
|
||||
]);
|
||||
|
||||
const dispose = client.dispose();
|
||||
spawnedProcesses[0]!.completeExit();
|
||||
await dispose;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user