11import { describe , expect , test } from "bun:test" ;
2+ import type { ConversationTurn } from "@intx/types/runtime" ;
3+ import { buildFleetDryContinuationPrompt } from "../subagent/fleet-dry-drive.js" ;
4+ import { buildMailboxMailPrompt } from "../subagent/mailbox-mail-drive.js" ;
25import {
36 EMPTY_PLAN_DETAIL ,
47 EMPTY_VIEW_DETAIL ,
@@ -8,6 +11,16 @@ import {
811 rowsFromHistoryBlocks ,
912 type HistoryBlock ,
1013} from "./history-hydrate.js" ;
14+ import { turnsToContentBlocks } from "./turns-to-blocks.js" ;
15+
16+ /** A persisted user turn: the reactor envelopes inbound text (createInboundTurn). */
17+ function envelopedUserTurn ( text : string ) : ConversationTurn {
18+ return {
19+ role : "user" ,
20+ content : [ { type : "text" , text : `[From: user@local]\n\n${ text } ` } ] ,
21+ timestamp : 0 ,
22+ } as unknown as ConversationTurn ;
23+ }
1124
1225describe ( "rowFromHistoryBlock" , ( ) => {
1326 test ( "user / text / reply / thinking" , ( ) => {
@@ -165,6 +178,68 @@ describe("rowFromHistoryBlock", () => {
165178 } ) ;
166179 } ) ;
167180
181+ test ( "persisted occupancy wakes drop only with their origin marker" , ( ) => {
182+ // turns-to-blocks marks wake-matching user turns origin:"system" at
183+ // persist time; the drop below is keyed on that marker, never the prefix
184+ // alone. Persisted turns carry the reactor envelope, so the fixtures use
185+ // the enveloped shape real sessions carry.
186+ const mail = `[From: user@local]\n\n${ buildMailboxMailPrompt ( [
187+ { agent_id : "w1" , status : "done" , report : "audit clean" } ,
188+ ] ) } `;
189+ expect (
190+ rowFromHistoryBlock ( { type : "user" , content : mail , origin : "system" } ) ,
191+ ) . toBeNull ( ) ;
192+
193+ const dry = `[From: user@local]\n\n${ buildFleetDryContinuationPrompt (
194+ [ { id : "t1" , title : "ship it" , status : "todo" } ] ,
195+ [ ] ,
196+ ) } `;
197+ expect (
198+ rowFromHistoryBlock ( { type : "user" , content : dry , origin : "system" } ) ,
199+ ) . toBeNull ( ) ;
200+
201+ // At this layer alone, the same bytes without the system marker paint:
202+ // the drop is keyed on the marker, never the prefix. That is a
203+ // layer-local guarantee, not the pipeline outcome — turns-to-blocks
204+ // marks any verbatim wake shape (operator-typed or not) before it
205+ // reaches here, so an enveloped verbatim-wake operator turn drops end
206+ // to end (locked by the pipeline test below). Only a bare prefix, which
207+ // the marker never matches, is guaranteed to paint on resume.
208+ expect ( rowFromHistoryBlock ( { type : "user" , content : mail } ) ) . toEqual ( {
209+ role : "user" ,
210+ text : mail ,
211+ } ) ;
212+
213+ // Explicitly operator-flagged text paints at this layer, even verbatim
214+ // wake text — again layer-local, not the pipeline outcome (see above).
215+ expect (
216+ rowFromHistoryBlock ( {
217+ type : "user" ,
218+ content : mail ,
219+ origin : "operator" ,
220+ } ) ,
221+ ) . toEqual ( { role : "user" , text : mail } ) ;
222+
223+ // Ordinary operator text is untouched.
224+ expect ( rowFromHistoryBlock ( { type : "user" , content : "hi" } ) ) . toEqual ( {
225+ role : "user" ,
226+ text : "hi" ,
227+ } ) ;
228+ } ) ;
229+
230+ test ( "non-wake system inbound still paints on resume" , ( ) => {
231+ // A system-originated inbound that is not an occupancy wake — shaped like
232+ // a background-shell completion notice (mailbox "system", no operator
233+ // flag) — paints live and must survive resume too. Only wakes carry the
234+ // origin marker, so this arrives unmarked and must paint.
235+ const notice =
236+ "[From: user@local]\n\nBackground shell abc123 finished: exit code 0.\ncommand: bun test\noutput:\n3 pass" ;
237+ expect ( rowFromHistoryBlock ( { type : "user" , content : notice } ) ) . toEqual ( {
238+ role : "user" ,
239+ text : notice ,
240+ } ) ;
241+ } ) ;
242+
168243 test ( "a tasks block no longer hydrates a row at all" , ( ) => {
169244 // Task state is live panel state, not conversation history. Nothing writes
170245 // this block any more, and an old session carrying one must not paint a
@@ -312,3 +387,43 @@ describe("hydrateHistoryRows", () => {
312387 ] ) ;
313388 } ) ;
314389} ) ;
390+
391+ describe ( "resume pipeline end to end (turns-to-blocks into hydrate)" , ( ) => {
392+ test ( "a marked wake drops while operator text paints" , ( ) => {
393+ // Mirrors runner wiring: persisted turns become content blocks, which
394+ // cross history.hydrate as untyped JSON — so the origin marker must
395+ // survive asHistoryBlock for the drop to fire, and unmarked text must
396+ // paint even though the wake prefix is in the same payload.
397+ const wake = buildMailboxMailPrompt ( [
398+ { agent_id : "w1" , status : "done" , report : "audit clean" } ,
399+ ] ) ;
400+ const operatorText = "[From: user@local]\n\nship it" ;
401+ const blocks = turnsToContentBlocks ( [
402+ envelopedUserTurn ( wake ) ,
403+ envelopedUserTurn ( "ship it" ) ,
404+ ] ) ;
405+ expect ( blocks ) . toMatchObject ( [
406+ { type : "user" , origin : "system" } ,
407+ { type : "user" } ,
408+ ] ) ;
409+ const rows = hydrateHistoryRows ( JSON . parse ( JSON . stringify ( blocks ) ) ) ;
410+ expect ( rows ) . toEqual ( [ { role : "user" , text : operatorText } ] ) ;
411+ } ) ;
412+
413+ test ( "an enveloped verbatim-wake operator turn drops end to end" , ( ) => {
414+ // Residual provenance gap: persisted turns carry no message flags, so
415+ // turns-to-blocks marks wakes by content and an operator turn carrying
416+ // a byte-verbatim wake (full wake line plus report JSON — a deliberate
417+ // paste) is marked origin:"system" and dropped here. Trigger is narrow
418+ // and the consequence cosmetic (one scrollback row; model history
419+ // intact), but the drop is real: this test fails if anyone claims
420+ // verbatim operator text always paints.
421+ const wake = buildMailboxMailPrompt ( [
422+ { agent_id : "w1" , status : "done" , report : "audit clean" } ,
423+ ] ) ;
424+ const blocks = turnsToContentBlocks ( [ envelopedUserTurn ( wake ) ] ) ;
425+ expect ( blocks ) . toMatchObject ( [ { type : "user" , origin : "system" } ] ) ;
426+ const rows = hydrateHistoryRows ( JSON . parse ( JSON . stringify ( blocks ) ) ) ;
427+ expect ( rows ) . toEqual ( [ ] ) ;
428+ } ) ;
429+ } ) ;
0 commit comments