import { describe, expect, it } from 'bun:test'; import { decideResume } from './websocket'; // The first test in api/chat, which had none. // // It covers the harness decision on reconnect — the one place where guessing wrong writes something // permanent. `endTurnIfAgentIsGone` appends a durable `cut-off` row, so a session misidentified here // gets "the agent went away" written into a turn that is running normally, and it survives every reload // because surviving reloads is what that row is for. // // The old code was `msg.model || DEFAULT_MODEL` with `DEFAULT_MODEL = 'claude-code'`, and it was masked // only by the client always happening to send `model`. These tests exist so the default cannot come back. describe('decideResume', () => { it("takes the server's own record over the client's claim", () => { // The socket does not get to re-declare a session's harness. This is the direction that matters: // believing the claim is how an OpenCode session ends up subscribed to the Claude sidecar. expect(decideResume('opencode/big-pickle', 'claude-code')).toEqual({ kind: 'known', model: 'opencode/big-pickle', }); }); it('adopts on the claim only when this process has no record of the session', () => { // An officer restart drops the in-memory session while the agent carries on, so the client is the // only one left who knows. Trusting it here is not the same as trusting it above. expect(decideResume(undefined, 'opencode/big-pickle')).toEqual({ kind: 'adopt', model: 'opencode/big-pickle' }); expect(decideResume(undefined, 'claude-code')).toEqual({ kind: 'adopt', model: 'claude-code' }); }); it('marks a total unknown as an assumption instead of passing it off as knowledge', () => { // No record and no claim. This still adopts — see below — but the caller can tell it apart, which is // what keeps the durable cut-off row off a session nobody has identified. expect(decideResume(undefined, undefined)).toEqual({ kind: 'assume', model: 'claude-code' }); expect(decideResume(undefined, '')).toEqual({ kind: 'assume', model: 'claude-code' }); }); it('always yields a model to adopt with, because an unbound socket misses the whole turn', () => { // Regression guard. Refusing to adopt when the harness was unknown looked principled and broke the // app inside an hour: `useChat.ts` only sends `model` `if (modelRef.current)`, so a reconnect // without one is routine. The socket never re-bound, the live turn's output went nowhere, and the // transcript collapsed to "turn completed without output" until a refresh rebuilt it from the log. for (const d of [ decideResume('claude-code', undefined), decideResume(undefined, 'opencode/x'), decideResume(undefined, undefined), ]) { expect(d.model).toBeTruthy(); } }); });