test(session-turn-outline): cover fold edges and deflake settle assertion

Edge-branch coverage for the preview reading bound, repeated and empty
drafts, draftless turn ends, orphan-draft clearing, and same-response
recommits (the CI per-file gate exercises them); the jump-settle spec
now asserts only the busy lifecycle after settlement — jsdom's zero
geometry made the rAF active-turn resync timing-dependent under
coverage instrumentation, and the landing position contract lives in
the browser e2e.
This commit is contained in:
Yichen Jiang 2026-08-30 14:39:39 +08:00 • committed by imccyu
parent 0e63841189
commit 7c58a95ebb
2 changed files with 48 additions and 1 deletions

View file

@ -720,8 +720,11 @@ describe('ChatView', () => {
// runs the final landing correction after the load-earlier button leaves.
expect(first.getAttribute('aria-busy')).toBe('true')
await act(async () => { releaseJump?.() })
// Only the busy lifecycle is asserted after settlement: jsdom's zero
// geometry makes the rAF active-turn resync read "at bottom" and hand the
// mark to the last turn, so aria-current here is timing-dependent under
// instrumentation; the landing position contract lives in the browser e2e.
expect(first.getAttribute('aria-busy')).toBeNull()
expect(first.getAttribute('aria-current')).toBe('true')
})
it('hands a windowless tool result to the Tool seat with an empty tool name', () => {

View file

@ -141,6 +141,50 @@ describe('turn outline projection unit', () => {
expect(changes.at(-1)?.last?.response).toBe('draft two')
})
it('keeps quiet on a draftless turn end and an empty in-turn prompt', async () => {
const { ctx, session } = await harness(true)
session.append('turn/start', { turn: 1 })
// Whitespace-only prompt text normalizes to nothing: the entry stays unlabeled.
appendPrompt(session, ' \t ')
endTurn(session, 1)
expect(outlineOf(ctx, session)).toEqual([{ turn: 1, seq: 0, prompt: '', response: '' }])
})
it('bounds preview reading and keeps repeated or empty drafts quiet (fabricated envelopes)', () => {
const def = turnOutlineProjectionDefinition
const assistant = (blocks: readonly unknown[]): SessionEvent => ({
type: 'assistant/message',
seq: 9,
time: 0,
data: { message: { content: blocks } },
}) as unknown as SessionEvent
const base: TurnOutlineState = { turns: [{ turn: 1, seq: 0, prompt: 'p', response: '' }], draft: '' }
// Non-text blocks are skipped; whitespace-heavy short blocks cross the raw
// reading bound early, so the collapsed (short) draft still marks the
// unread remainder with an ellipsis.
const airy = Array.from({ length: 40 }, (_, index) => ({ type: 'text', text: `w${String(index)}${' '.repeat(20)}` }))
const buffered = def.apply(base, assistant([{ type: 'tool-call' }, ...airy]))
expect(buffered.draft.startsWith('w0 w1 ')).toBe(true)
expect(buffered.draft.endsWith('…')).toBe(true)
expect(buffered.draft.length).toBeLessThan(120)
// The same draft again, or a text-free message, changes nothing.
expect(def.apply(buffered, assistant([{ type: 'tool-call' }, ...airy]))).toBe(buffered)
expect(def.apply(buffered, assistant([{ type: 'text', text: ' ' }]))).toBe(buffered)
// A draft with no entry to commit into clears itself at the boundary…
const end = {
type: 'turn/end',
seq: 11,
time: 0,
data: { turn: 1, reason: { kind: 'completed' } },
} as unknown as SessionEvent
expect(def.apply({ turns: [], draft: 'orphan' }, end)).toEqual({ turns: [], draft: '' })
// …and a re-settled identical response keeps the entries' identity.
const settled: TurnOutlineState = { turns: [{ turn: 1, seq: 0, prompt: 'p', response: 'done' }], draft: 'done' }
const recommitted = def.apply(settled, end)
expect(recommitted.turns).toBe(settled.turns)
expect(recommitted.draft).toBe('')
})
it('skips a boundary that does not advance the turn number (fabricated envelope)', () => {
const state: TurnOutlineState = { turns: [{ turn: 2, seq: 5, prompt: 'kept', response: '' }], draft: '' }
const regressive = {