import { describe, expect, test } from 'bun:test'; import { countsAsOutput, diagnoseExit, stderrReason } from './exit-diagnosis'; // The bug these encode: a member's chat answered "Claude Code process exited with code 1" four times in a // row and could never recover, because every message retried the same `--resume` id and nothing ever // reconsidered it. A fresh chat worked throughout, which is what identifies the resume as the difference. describe('diagnoseExit', () => { test('a resuming turn that died before saying anything gives up on the resume', () => { const d = diagnoseExit({ sawOutput: false, resumeId: '49527b33', stderrTail: '' }); expect(d.clearResume).toBe(true); expect(d.message).toContain('could not reopen'); // The cost has to be stated: the next turn genuinely will not have the history. expect(d.message).toContain('will not remember'); }); // The important negative. A turn that spoke and then broke has a working transcript, so discarding the // resume would turn one failed turn into a permanently amnesiac conversation. test('a turn that produced output first keeps its conversation', () => { const d = diagnoseExit({ sawOutput: true, resumeId: '49527b33', stderrTail: 'boom' }); expect(d.clearResume).toBe(false); expect(d.message).toContain('conversation is safe'); }); test('a NEW session that dies has no resume to blame, so nothing is cleared', () => { const d = diagnoseExit({ sawOutput: false, resumeId: undefined, stderrTail: '' }); expect(d.clearResume).toBe(false); expect(d.message).toContain('exited unexpectedly'); }); test("the child's own words are carried to the person, not just to the log", () => { const d = diagnoseExit({ sawOutput: false, resumeId: 'abc', stderrTail: 'No conversation found with session ID' }); expect(d.message).toContain('No conversation found with session ID'); }); // A silent exit is the case that started this: code 1, nothing on stderr. The sentence still has to read. test('a silent death produces a clean sentence rather than a dangling colon', () => { const d = diagnoseExit({ sawOutput: false, resumeId: 'abc', stderrTail: ' \n \n' }); expect(d.message).not.toContain(':'); expect(d.message).toContain('could not reopen this conversation.'); }); }); describe('stderrReason', () => { test('keeps the last lines, where the actual error is', () => { expect(stderrReason('one\ntwo\nthree\nfour')).toBe('two three four'); }); test('is empty for whitespace, so callers can test it as a condition', () => { expect(stderrReason('')).toBe(''); expect(stderrReason(' \n \t ')).toBe(''); }); test('stays short enough to sit inside a sentence', () => { expect(stderrReason('x'.repeat(5000)).length).toBeLessThanOrEqual(300); }); }); // ── What counts as the harness having worked ── // // These exist because the first version of this rule was wrong in the only case it was written for, and // nothing failed: a broken resume emits a terminal error result, that was counted as output, and the // recovery it was supposed to trigger could never fire. describe('countsAsOutput', () => { test('an error does NOT count — it is what a failed resume emits', () => { expect(countsAsOutput('error')).toBe(false); }); test('real work counts', () => { for (const type of ['assistant:text', 'assistant:delta', 'tool:start', 'tool:result', 'result']) { expect(countsAsOutput(type)).toBe(true); } }); test('a stopped turn counts — it was running, so the conversation loaded', () => { // Clearing someone's transcript because they pressed stop quickly would be its own bug. expect(countsAsOutput('stopped')).toBe(true); }); // The end-to-end shape of the reported failure: no real output, an error result, a resuming turn. test('the reported failure now resolves to "give up on the resume"', () => { const events = ['error']; const sawOutput = events.some(countsAsOutput); expect(diagnoseExit({ sawOutput, resumeId: '49527b33', stderrTail: '' }).clearResume).toBe(true); }); test('and a turn that spoke before failing does not', () => { const events = ['assistant:text', 'error']; const sawOutput = events.some(countsAsOutput); expect(diagnoseExit({ sawOutput, resumeId: '49527b33', stderrTail: '' }).clearResume).toBe(false); }); });