feat: surface workflow diagnostics

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-01 18:50:56 +02:00
co-authored by Copilot
parent b434dd86b4
commit 11b36827f5
12 changed files with 527 additions and 4 deletions
@@ -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));
});
});
+97 -1
View File
@@ -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;
});
});