Regression from my own B7 change, reported within the hour: turns collapsing to "turn completed without output" and coming back only on refresh. B7 stopped resume-cursor defaulting an unidentified session to claude-code. Correct for the durable cut-off row, wrong for adoption: useChat sends model only if modelRef.current is set, so a reconnect without one is routine, not exotic. Declining to adopt left the socket unbound to the live session, so the running turn output went nowhere — and a refresh looked like a fix because it rebuilds from the durable log. Adoption is about DELIVERY and must be generous; only the durable write needs certainty. So adopt on the default again, mark it as an assumption, and skip the cut-off check on it. That keeps B7 fixed — no false "agent went away" written against an opencode session — with no unbound sockets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
2.8 KiB
TypeScript
52 lines
2.8 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|