11import fs from "node:fs" ;
22import os from "node:os" ;
33import path from "node:path" ;
4- import { afterEach , beforeEach , describe , expect , it } from "vitest" ;
4+ import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
55import { appendTranscriptEvent , upsertSessionEntry } from "../config/sessions/session-accessor.js" ;
66import { loadSessionStore } from "../config/sessions/store.js" ;
77import { withOwnedSessionTranscriptWrites } from "../config/sessions/transcript-write-context.js" ;
8+ import * as transcriptEvents from "../sessions/transcript-events.js" ;
89import {
910 appendSessionTranscriptMessageByIdentity ,
1011 formatSessionTranscriptMemoryHitKey ,
@@ -26,6 +27,7 @@ describe("session transcript runtime SDK", () => {
2627 } ) ;
2728
2829 afterEach ( ( ) => {
30+ vi . restoreAllMocks ( ) ;
2931 fs . rmSync ( tempDir , { force : true , recursive : true } ) ;
3032 } ) ;
3133
@@ -221,6 +223,89 @@ describe("session transcript runtime SDK", () => {
221223 ] ) ;
222224 } ) ;
223225
226+ it ( "publishes queued locked updates after callback appends are visible" , async ( ) => {
227+ const scope = {
228+ agentId : "main" ,
229+ sessionFile : path . join ( tempDir , "queued-publish-target.jsonl" ) ,
230+ sessionId : "queued-publish-session" ,
231+ sessionKey : "agent:main:main" ,
232+ storePath,
233+ } ;
234+ const observedUpdates : Array < {
235+ callbackCompleted : boolean ;
236+ fileText : string ;
237+ update : unknown ;
238+ } > = [ ] ;
239+ let callbackCompleted = false ;
240+ const emitSpy = vi
241+ . spyOn ( transcriptEvents , "emitSessionTranscriptUpdate" )
242+ . mockImplementation ( ( update ) => {
243+ observedUpdates . push ( {
244+ callbackCompleted,
245+ fileText : fs . readFileSync ( scope . sessionFile , "utf8" ) ,
246+ update,
247+ } ) ;
248+ } ) ;
249+
250+ const result = await withSessionTranscriptWriteLock ( scope , async ( locked ) => {
251+ await locked . appendMessage ( {
252+ message : {
253+ role : "assistant" ,
254+ content : [ { type : "text" , text : "queued publish" } ] ,
255+ timestamp : 1 ,
256+ } ,
257+ } ) ;
258+ await locked . publishUpdate ( {
259+ messageId : "message-from-callback" ,
260+ sessionKey : scope . sessionKey ,
261+ } ) ;
262+ expect ( emitSpy ) . not . toHaveBeenCalled ( ) ;
263+ callbackCompleted = true ;
264+ return "complete" ;
265+ } ) ;
266+
267+ expect ( result ) . toBe ( "complete" ) ;
268+ expect ( emitSpy ) . toHaveBeenCalledTimes ( 1 ) ;
269+ expect ( observedUpdates ) . toEqual ( [
270+ {
271+ callbackCompleted : true ,
272+ fileText : expect . stringContaining ( "queued publish" ) ,
273+ update : expect . objectContaining ( {
274+ messageId : "message-from-callback" ,
275+ sessionFile : scope . sessionFile ,
276+ sessionKey : scope . sessionKey ,
277+ } ) ,
278+ } ,
279+ ] ) ;
280+ } ) ;
281+
282+ it ( "does not publish queued locked updates when the callback throws" , async ( ) => {
283+ const scope = {
284+ agentId : "main" ,
285+ sessionFile : path . join ( tempDir , "failed-queued-publish-target.jsonl" ) ,
286+ sessionId : "failed-queued-publish-session" ,
287+ sessionKey : "agent:main:main" ,
288+ storePath,
289+ } ;
290+ const emitSpy = vi . spyOn ( transcriptEvents , "emitSessionTranscriptUpdate" ) ;
291+
292+ await expect (
293+ withSessionTranscriptWriteLock ( scope , async ( locked ) => {
294+ await locked . appendMessage ( {
295+ message : {
296+ role : "assistant" ,
297+ content : [ { type : "text" , text : "durable but failed" } ] ,
298+ timestamp : 1 ,
299+ } ,
300+ } ) ;
301+ await locked . publishUpdate ( { sessionKey : scope . sessionKey } ) ;
302+ throw new Error ( "stop before commit" ) ;
303+ } ) ,
304+ ) . rejects . toThrow ( "stop before commit" ) ;
305+ expect ( emitSpy ) . not . toHaveBeenCalled ( ) ;
306+ expect ( fs . readFileSync ( scope . sessionFile , "utf8" ) ) . toContain ( "durable but failed" ) ;
307+ } ) ;
308+
224309 it ( "round-trips encoded memory hit keys with opaque session ids" , ( ) => {
225310 const key = formatSessionTranscriptMemoryHitKey ( {
226311 agentId : "SECONDARY" ,
0 commit comments