Keep OpenCode app MCP transport evidence durable, refresh stale sessions without consuming normal delivery attempts, and keep recoverable runtime diagnostics out of member card errors. Cover stable MCP restart/fallback, forced session refresh, resolved_behavior_changed recovery, and renderer diagnostics with regression and safe e2e tests.
241 lines
8.9 KiB
TypeScript
241 lines
8.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
buildMemberLaunchDiagnosticsPayload,
|
|
formatMemberLaunchDiagnosticsPayload,
|
|
hasMemberLaunchDiagnosticsDetails,
|
|
getMemberLaunchDiagnosticsErrorMessage,
|
|
} from '@renderer/utils/memberLaunchDiagnostics';
|
|
|
|
describe('member launch diagnostics', () => {
|
|
it('builds a bounded copy payload from spawn and runtime evidence', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
teamName: 'demo-team',
|
|
runId: 'run-42',
|
|
memberName: 'bob',
|
|
spawnEntry: {
|
|
status: 'waiting',
|
|
launchState: 'runtime_pending_bootstrap',
|
|
runtimeAlive: false,
|
|
bootstrapConfirmed: false,
|
|
hardFailure: false,
|
|
agentToolAccepted: true,
|
|
livenessKind: 'shell_only',
|
|
livenessSource: 'process',
|
|
runtimeDiagnostic: 'tmux pane foreground command is zsh',
|
|
runtimeDiagnosticSeverity: 'warning',
|
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
|
},
|
|
runtimeEntry: {
|
|
memberName: 'bob',
|
|
alive: false,
|
|
restartable: true,
|
|
pid: 26676,
|
|
pidSource: 'tmux_pane',
|
|
paneId: '%42',
|
|
panePid: 26676,
|
|
paneCurrentCommand: 'zsh',
|
|
processCommand: 'node runtime --token super-secret --team-name demo-team',
|
|
diagnostics: ['tmux pane foreground command is zsh', 'no runtime child found'],
|
|
updatedAt: '2026-04-24T12:00:01.000Z',
|
|
},
|
|
});
|
|
|
|
expect(payload).toMatchObject({
|
|
teamName: 'demo-team',
|
|
runId: 'run-42',
|
|
memberName: 'bob',
|
|
launchState: 'runtime_pending_bootstrap',
|
|
spawnStatus: 'waiting',
|
|
livenessKind: 'shell_only',
|
|
pid: 26676,
|
|
pidSource: 'tmux_pane',
|
|
paneCurrentCommand: 'zsh',
|
|
runtimeDiagnostic: 'tmux pane foreground command is zsh',
|
|
runtimeDiagnosticSeverity: 'warning',
|
|
});
|
|
expect(payload.processCommand).toContain('--token [redacted]');
|
|
expect(payload.processCommand).not.toContain('super-secret');
|
|
expect(payload.diagnostics).toEqual([
|
|
'tmux pane foreground command is zsh',
|
|
'no runtime child found',
|
|
]);
|
|
expect(hasMemberLaunchDiagnosticsDetails(payload)).toBe(true);
|
|
expect(formatMemberLaunchDiagnosticsPayload(payload)).toContain('"livenessKind": "shell_only"');
|
|
});
|
|
|
|
it('includes the exact normalized member card error in copy diagnostics', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
memberName: 'jack',
|
|
spawnEntry: {
|
|
status: 'error',
|
|
launchState: 'failed_to_start',
|
|
hardFailure: true,
|
|
hardFailureReason:
|
|
'Latest assistant message msg_123 failed with APIError - OpenCode quota exhausted. Visit https://openrouter.ai/settings/keys',
|
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
|
runtimeDiagnosticSeverity: 'error',
|
|
updatedAt: '2026-05-08T12:00:00.000Z',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBe(
|
|
'OpenCode quota exhausted. Visit https://openrouter.ai/settings/keys'
|
|
);
|
|
expect(payload.diagnostics?.[0]).toBe(
|
|
'OpenCode quota exhausted. Visit https://openrouter.ai/settings/keys'
|
|
);
|
|
expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBe(
|
|
'OpenCode quota exhausted. Visit https://openrouter.ai/settings/keys'
|
|
);
|
|
expect(formatMemberLaunchDiagnosticsPayload(payload)).toContain('"memberCardError"');
|
|
});
|
|
|
|
it('includes runtime advisory evidence in copy diagnostics', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
memberName: 'alice',
|
|
runtimeAdvisoryLabel: 'OpenCode delivery error',
|
|
runtimeAdvisoryTitle: 'OpenCode accepted the prompt, but no assistant turn was recorded.',
|
|
runtimeAdvisory: {
|
|
kind: 'api_error',
|
|
observedAt: '2026-05-17T22:11:38.239Z',
|
|
reasonCode: 'backend_error',
|
|
message: 'OpenCode accepted the prompt, but no assistant turn was recorded.',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBe(
|
|
'OpenCode accepted the prompt, but no assistant turn was recorded.'
|
|
);
|
|
expect(payload.runtimeAdvisoryKind).toBe('api_error');
|
|
expect(payload.runtimeAdvisoryReasonCode).toBe('backend_error');
|
|
expect(payload.diagnostics).toContain(
|
|
'OpenCode accepted the prompt, but no assistant turn was recorded.'
|
|
);
|
|
expect(hasMemberLaunchDiagnosticsDetails(payload)).toBe(true);
|
|
});
|
|
|
|
it('does not turn healthy info liveness diagnostics into member card errors', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
teamName: 'atlas-hq-5',
|
|
runId: '5a9ee2e5-a8cb-4559-b624-0dbf13ee4d11',
|
|
memberName: 'atlas',
|
|
spawnEntry: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
agentToolAccepted: true,
|
|
livenessKind: 'runtime_process',
|
|
livenessSource: 'heartbeat',
|
|
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
|
|
runtimeDiagnosticSeverity: 'info',
|
|
updatedAt: '2026-05-18T08:13:23.902Z',
|
|
},
|
|
runtimeEntry: {
|
|
memberName: 'atlas',
|
|
providerId: 'opencode',
|
|
alive: true,
|
|
restartable: false,
|
|
livenessKind: 'runtime_process',
|
|
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
|
|
runtimeDiagnosticSeverity: 'info',
|
|
diagnostics: [
|
|
'OpenCode runtime process detected after bootstrap confirmation',
|
|
'matched OpenCode runtime pid and process identity',
|
|
'bootstrap confirmed',
|
|
],
|
|
updatedAt: '2026-05-18T08:34:47.845Z',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBeUndefined();
|
|
expect(payload.runtimeDiagnostic).toBe(
|
|
'OpenCode runtime process detected after bootstrap confirmation'
|
|
);
|
|
expect(payload.runtimeDiagnosticSeverity).toBe('info');
|
|
expect(payload.diagnostics).toContain(
|
|
'OpenCode runtime process detected after bootstrap confirmation'
|
|
);
|
|
});
|
|
|
|
it('prefers advisory errors over healthy info liveness diagnostics', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
memberName: 'atlas',
|
|
runtimeAdvisoryLabel: 'OpenCode delivery error',
|
|
runtimeAdvisoryTitle:
|
|
'OpenCode runtime delivery error. OpenCode accepted the prompt, but no assistant turn was recorded.',
|
|
spawnEntry: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
livenessKind: 'runtime_process',
|
|
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
|
|
runtimeDiagnosticSeverity: 'info',
|
|
updatedAt: '2026-05-18T08:13:23.902Z',
|
|
},
|
|
runtimeAdvisory: {
|
|
kind: 'api_error',
|
|
observedAt: '2026-05-18T08:31:46.075Z',
|
|
reasonCode: 'backend_error',
|
|
message: 'OpenCode accepted the prompt, but no assistant turn was recorded.',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBe(
|
|
'OpenCode runtime delivery error. OpenCode accepted the prompt, but no assistant turn was recorded.'
|
|
);
|
|
expect(payload.memberCardError).not.toBe(
|
|
'OpenCode runtime process detected after bootstrap confirmation'
|
|
);
|
|
});
|
|
|
|
it('does not surface recoverable OpenCode session refresh advisory as card error', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
memberName: 'tom',
|
|
runtimeAdvisoryLabel: 'OpenCode session refresh',
|
|
runtimeAdvisoryTitle: 'OpenCode session changed; refreshing the session before retry.',
|
|
spawnEntry: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
|
|
runtimeDiagnosticSeverity: 'info',
|
|
updatedAt: '2026-05-18T08:13:23.902Z',
|
|
},
|
|
runtimeAdvisory: {
|
|
kind: 'api_error',
|
|
observedAt: '2026-05-18T08:31:46.075Z',
|
|
reasonCode: 'backend_error',
|
|
message: 'OpenCode session changed; refreshing the session before retry.',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBeUndefined();
|
|
expect(payload.diagnostics).toContain(
|
|
'OpenCode session changed; refreshing the session before retry.'
|
|
);
|
|
});
|
|
|
|
it('does not surface recoverable OpenCode transport refresh advisory as card error', () => {
|
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
|
memberName: 'tom',
|
|
runtimeAdvisoryLabel: 'OpenCode session refresh',
|
|
runtimeAdvisoryTitle:
|
|
'OpenCode session changed; refreshing the session before retry.',
|
|
runtimeAdvisory: {
|
|
kind: 'api_error',
|
|
observedAt: '2026-05-18T08:31:46.075Z',
|
|
reasonCode: 'backend_error',
|
|
message: 'opencode_app_mcp_transport_changed:old->new',
|
|
},
|
|
});
|
|
|
|
expect(payload.memberCardError).toBeUndefined();
|
|
});
|
|
});
|