240 lines
6.3 KiB
TypeScript
240 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
mapOpenCodeStatusToDurableState,
|
|
normalizeOpenCodeEvent,
|
|
normalizeOpenCodeSessionStatus,
|
|
} from '../../../../src/main/services/team/opencode/events/OpenCodeEventNormalizer';
|
|
|
|
describe('OpenCodeEventNormalizer', () => {
|
|
it('normalizes v1.14 session.status object', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
status: { type: 'retry', attempt: 2, message: 'rate limited', next: 123 },
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'session_status',
|
|
sessionId: 'ses_1',
|
|
status: {
|
|
type: 'retry',
|
|
retryAttempt: 2,
|
|
retryMessage: 'rate limited',
|
|
retryNextAt: 123,
|
|
rawShape: 'v1.14',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('normalizes legacy string status and active compatibility status', () => {
|
|
expect(normalizeOpenCodeSessionStatus('active')).toMatchObject({
|
|
type: 'busy',
|
|
rawShape: 'legacy-string',
|
|
});
|
|
expect(normalizeOpenCodeSessionStatus('idle')).toMatchObject({
|
|
type: 'idle',
|
|
rawShape: 'legacy-string',
|
|
});
|
|
expect(normalizeOpenCodeSessionStatus('unexpected')).toMatchObject({
|
|
type: 'unknown',
|
|
rawShape: 'legacy-string',
|
|
});
|
|
});
|
|
|
|
it('normalizes deprecated session.idle as an idle session status', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'session.idle',
|
|
properties: { sessionID: 'ses_1' },
|
|
})
|
|
).toMatchObject({
|
|
kind: 'session_status',
|
|
sessionId: 'ses_1',
|
|
status: {
|
|
type: 'idle',
|
|
rawShape: 'v1.14',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('normalizes global event envelopes without losing directory evidence', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
directory: '/repo',
|
|
payload: {
|
|
type: 'server.heartbeat',
|
|
properties: {},
|
|
},
|
|
})
|
|
).toEqual({
|
|
kind: 'server_heartbeat',
|
|
scope: 'global',
|
|
directory: '/repo',
|
|
raw: {
|
|
directory: '/repo',
|
|
payload: {
|
|
type: 'server.heartbeat',
|
|
properties: {},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('normalizes message.updated role and message id from info snapshot', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'message.updated',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
info: { id: 'msg_1', role: 'assistant' },
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'message_updated',
|
|
sessionId: 'ses_1',
|
|
messageId: 'msg_1',
|
|
role: 'assistant',
|
|
});
|
|
});
|
|
|
|
it('normalizes message.part.updated snapshots separately from streaming deltas', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'message.part.updated',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
part: {
|
|
id: 'part_1',
|
|
messageID: 'msg_1',
|
|
type: 'text',
|
|
text: 'complete text',
|
|
},
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'message_part_updated',
|
|
sessionId: 'ses_1',
|
|
messageId: 'msg_1',
|
|
partId: 'part_1',
|
|
partType: 'text',
|
|
textSnapshot: 'complete text',
|
|
});
|
|
});
|
|
|
|
it('normalizes streaming text from message.part.delta', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'message.part.delta',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
field: 'text',
|
|
delta: 'hello',
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'message_part_delta',
|
|
sessionId: 'ses_1',
|
|
messageId: 'msg_1',
|
|
partId: 'part_1',
|
|
field: 'text',
|
|
delta: 'hello',
|
|
});
|
|
});
|
|
|
|
it('normalizes permission events across v1.14 and legacy ids', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'permission.asked',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
id: 'perm_1',
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'permission_asked',
|
|
sessionId: 'ses_1',
|
|
requestId: 'perm_1',
|
|
});
|
|
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'permission.replied',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
requestID: 'perm_legacy',
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'permission_replied',
|
|
sessionId: 'ses_1',
|
|
requestId: 'perm_legacy',
|
|
});
|
|
});
|
|
|
|
it('returns unknown event instead of throwing on incomplete known payloads', () => {
|
|
expect(
|
|
normalizeOpenCodeEvent({
|
|
type: 'message.part.delta',
|
|
properties: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
field: 'text',
|
|
delta: 'hello',
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
kind: 'unknown',
|
|
type: 'message.part.delta',
|
|
});
|
|
});
|
|
|
|
it('maps normalized status and projections to durable session state', () => {
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
|
|
hasPendingPermission: true,
|
|
hasLatestAssistantError: false,
|
|
replyPendingSinceMessageId: null,
|
|
})
|
|
).toBe('blocked');
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
|
|
hasPendingPermission: false,
|
|
hasLatestAssistantError: true,
|
|
replyPendingSinceMessageId: null,
|
|
})
|
|
).toBe('error');
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'retry' }), {
|
|
hasPendingPermission: false,
|
|
hasLatestAssistantError: false,
|
|
replyPendingSinceMessageId: null,
|
|
})
|
|
).toBe('retrying');
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
|
|
hasPendingPermission: false,
|
|
hasLatestAssistantError: false,
|
|
replyPendingSinceMessageId: null,
|
|
})
|
|
).toBe('running');
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'idle' }), {
|
|
hasPendingPermission: false,
|
|
hasLatestAssistantError: false,
|
|
replyPendingSinceMessageId: 'msg_1',
|
|
})
|
|
).toBe('reply_pending');
|
|
expect(
|
|
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'idle' }), {
|
|
hasPendingPermission: false,
|
|
hasLatestAssistantError: false,
|
|
replyPendingSinceMessageId: null,
|
|
})
|
|
).toBe('idle');
|
|
});
|
|
});
|