Reconnects and resyncs active sessions when live updates stall Normalizes synthetic session status events Uses authoritative status snapshots to clear stale busy states
152 lines
3.8 KiB
JavaScript
152 lines
3.8 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createSessionRuntime } from './session-runtime.js';
|
|
|
|
describe('session runtime', () => {
|
|
const runtimes = [];
|
|
|
|
afterEach(() => {
|
|
for (const runtime of runtimes) {
|
|
runtime.dispose();
|
|
}
|
|
runtimes.length = 0;
|
|
});
|
|
|
|
it('broadcasts attention clears through the shared broadcaster', () => {
|
|
const events = [];
|
|
const runtime = createSessionRuntime({
|
|
writeSseEvent() {
|
|
throw new Error('SSE fallback should not be used when broadcastEvent is provided');
|
|
},
|
|
getNotificationClients: () => new Set(),
|
|
broadcastEvent: (payload) => {
|
|
events.push(payload);
|
|
},
|
|
});
|
|
runtimes.push(runtime);
|
|
|
|
runtime.processOpenCodeSsePayload({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'session-1',
|
|
status: {
|
|
type: 'busy',
|
|
},
|
|
},
|
|
});
|
|
runtime.markUserMessageSent('session-1');
|
|
runtime.processOpenCodeSsePayload({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'session-1',
|
|
status: {
|
|
type: 'idle',
|
|
},
|
|
},
|
|
});
|
|
runtime.markSessionViewed('session-1', 'client-1');
|
|
|
|
expect(events).toContainEqual({
|
|
type: 'openchamber:session-status',
|
|
properties: expect.objectContaining({
|
|
sessionID: 'session-1',
|
|
status: 'idle',
|
|
needsAttention: true,
|
|
}),
|
|
});
|
|
expect(events.at(-1)).toEqual({
|
|
type: 'openchamber:session-status',
|
|
properties: {
|
|
sessionID: 'session-1',
|
|
status: 'idle',
|
|
timestamp: expect.any(Number),
|
|
metadata: {},
|
|
needsAttention: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('accepts legacy session.status info.type payloads', () => {
|
|
const events = [];
|
|
const runtime = createSessionRuntime({
|
|
writeSseEvent() {
|
|
throw new Error('SSE fallback should not be used when broadcastEvent is provided');
|
|
},
|
|
getNotificationClients: () => new Set(),
|
|
broadcastEvent: (payload) => {
|
|
events.push(payload);
|
|
},
|
|
});
|
|
runtimes.push(runtime);
|
|
|
|
runtime.processOpenCodeSsePayload({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'legacy-session-1',
|
|
info: {
|
|
type: 'busy',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(events).toContainEqual({
|
|
type: 'openchamber:session-status',
|
|
properties: expect.objectContaining({
|
|
sessionID: 'legacy-session-1',
|
|
status: 'busy',
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('broadcasts idle activity when cooldown expires', () => {
|
|
vi.useFakeTimers();
|
|
const events = [];
|
|
const runtime = createSessionRuntime({
|
|
writeSseEvent() {
|
|
throw new Error('SSE fallback should not be used when broadcastEvent is provided');
|
|
},
|
|
getNotificationClients: () => new Set(),
|
|
broadcastEvent: (payload) => {
|
|
events.push(payload);
|
|
},
|
|
});
|
|
|
|
try {
|
|
runtime.processOpenCodeSsePayload({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'session-activity-1',
|
|
status: {
|
|
type: 'busy',
|
|
},
|
|
},
|
|
});
|
|
runtime.processOpenCodeSsePayload({
|
|
type: 'session.status',
|
|
properties: {
|
|
sessionID: 'session-activity-1',
|
|
status: {
|
|
type: 'idle',
|
|
},
|
|
},
|
|
});
|
|
|
|
const activityPhases = () => events
|
|
.filter((event) => event.type === 'openchamber:session-activity')
|
|
.map((event) => event.properties.phase);
|
|
|
|
expect(activityPhases()).toEqual(['busy', 'cooldown']);
|
|
|
|
vi.advanceTimersByTime(1999);
|
|
expect(activityPhases()).toEqual(['busy', 'cooldown']);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
|
|
expect(activityPhases()).toEqual(['busy', 'cooldown', 'idle']);
|
|
} finally {
|
|
runtime.dispose();
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|