1- import { randomUUID } from 'node:crypto' ;
1+ import { createHash , randomUUID } from 'node:crypto' ;
22import {
33 mkdir ,
44 mkdtemp ,
@@ -9,14 +9,17 @@ import {
99 writeFile ,
1010} from 'node:fs/promises' ;
1111import { tmpdir } from 'node:os' ;
12- import { join } from 'node:path' ;
12+ import { join , resolve } from 'node:path' ;
1313
1414import { afterEach , describe , expect , it } from 'vitest' ;
1515
1616import {
17- withRawTranscriptSessionMarkerLock ,
17+ commitRawTranscriptSessionCheckpoint ,
18+ getRawTranscriptSessionMarkerPath ,
19+ type RawTranscriptSessionCommitOptions ,
1820 type RawTranscriptSessionLockOptions ,
1921} from '../src/processing/tail.js' ;
22+ import type { RawTranscriptSessionCheckpoint } from '../src/processing/types.js' ;
2023
2124const tempRoots : string [ ] = [ ] ;
2225const STALE_MS = 30_000 ;
@@ -42,13 +45,72 @@ afterEach(async () => {
4245
4346async function fixture ( label : string ) : Promise < {
4447 root : string ;
48+ mainPath : string ;
4549 markerPath : string ;
4650 lockPath : string ;
4751} > {
4852 const root = await mkdtemp ( join ( tmpdir ( ) , `raw-transcript-lock-${ label } -` ) ) ;
4953 tempRoots . push ( root ) ;
50- const markerPath = join ( root , 'session.marker.json' ) ;
51- return { root, markerPath, lockPath : `${ markerPath } .lock` } ;
54+ const mainPath = join ( root , 'session.jsonl' ) ;
55+ const markerDir = join ( root , 'markers' ) ;
56+ await mkdir ( markerDir , { recursive : true , mode : 0o700 } ) ;
57+ await writeFile ( mainPath , '' ) ;
58+ const markerPath = getRawTranscriptSessionMarkerPath ( mainPath , markerDir , [
59+ root ,
60+ ] ) ;
61+ return { root, mainPath, markerPath, lockPath : `${ markerPath } .lock` } ;
62+ }
63+
64+ function commitOptions (
65+ root : string ,
66+ extras : Omit <
67+ RawTranscriptSessionCommitOptions ,
68+ 'markerDir' | 'allowedMarkerRoots'
69+ > = { }
70+ ) : RawTranscriptSessionCommitOptions {
71+ return {
72+ markerDir : join ( root , 'markers' ) ,
73+ allowedMarkerRoots : [ root ] ,
74+ ...extras ,
75+ } ;
76+ }
77+
78+ function checkpoint (
79+ mainPath : string ,
80+ baseRevision : number
81+ ) : RawTranscriptSessionCheckpoint {
82+ return {
83+ sessionId : 'session' ,
84+ mainPathDigest : createHash ( 'sha256' )
85+ . update ( resolve ( mainPath ) )
86+ . digest ( 'hex' ) ,
87+ baseRevision,
88+ sources : [
89+ {
90+ sourceKind : 'main' ,
91+ sourceId : 'main' ,
92+ generation : 0 ,
93+ byteOffset : 0 ,
94+ fileSize : 0 ,
95+ } ,
96+ ] ,
97+ } ;
98+ }
99+
100+ function commitCheckpoint (
101+ mainPath : string ,
102+ root : string ,
103+ baseRevision : number ,
104+ extras : Omit <
105+ RawTranscriptSessionCommitOptions ,
106+ 'markerDir' | 'allowedMarkerRoots'
107+ > = { }
108+ ) : Promise < void > {
109+ return commitRawTranscriptSessionCheckpoint (
110+ mainPath ,
111+ checkpoint ( mainPath , baseRevision ) ,
112+ commitOptions ( root , extras )
113+ ) ;
52114}
53115
54116async function tokenSnapshot (
@@ -91,40 +153,38 @@ function errorMessage(error: unknown): string {
91153
92154describe ( 'raw transcript session lock three-party schedules' , ( ) => {
93155 it ( 'T1 keeps fresh owner B canonical while displaced owner A releases' , async ( ) => {
94- const { markerPath , lockPath } = await fixture ( 't1-displaced-owner' ) ;
156+ const { root , mainPath , lockPath } = await fixture ( 't1-displaced-owner' ) ;
95157 const holdA = deferred ( ) ;
96158 const aEntered = deferred ( ) ;
97159 const releaseBarrier = deferred ( ) ;
98160 const aReleasePaused = deferred ( ) ;
99161 const holdB = deferred ( ) ;
100162 const bEntered = deferred ( ) ;
101163
102- const ownerA = withRawTranscriptSessionMarkerLock (
103- markerPath ,
104- async ( ) => {
105- aEntered . resolve ( ) ;
106- await holdA . promise ;
107- } ,
108- {
164+ const ownerA = commitCheckpoint ( mainPath , root , 0 , {
165+ lock : {
109166 onAfterReleaseTokensUnlinkedBeforeRmdir : async ( ) => {
110167 aReleasePaused . resolve ( ) ;
111168 await releaseBarrier . promise ;
112169 } ,
113- }
114- ) ;
170+ } ,
171+ onLocked : async ( ) => {
172+ aEntered . resolve ( ) ;
173+ await holdA . promise ;
174+ } ,
175+ } ) ;
115176 await aEntered . promise ;
116177
117- const ownerB = withRawTranscriptSessionMarkerLock (
118- markerPath ,
119- async ( ) => {
178+ const ownerB = commitCheckpoint ( mainPath , root , 1 , {
179+ lock : {
180+ now : ( ) => Date . now ( ) + STALE_MS + 5_000 ,
181+ isProcessAlive : ( ) => false ,
182+ } ,
183+ onLocked : async ( ) => {
120184 bEntered . resolve ( ) ;
121185 await holdB . promise ;
122186 } ,
123- {
124- now : ( ) => Date . now ( ) + STALE_MS + 5_000 ,
125- isProcessAlive : ( ) => false ,
126- }
127- ) ;
187+ } ) ;
128188 await bEntered . promise ;
129189 const ownerBBeforeRelease = await tokenSnapshot ( lockPath ) ;
130190
@@ -149,51 +209,48 @@ describe('raw transcript session lock three-party schedules', () => {
149209 } ) ;
150210
151211 it ( 'T2 rejects interloper C while displaced owner A release is in flight' , async ( ) => {
152- const { markerPath } = await fixture ( 't2-release-interloper' ) ;
212+ const { root , mainPath } = await fixture ( 't2-release-interloper' ) ;
153213 const holdA = deferred ( ) ;
154214 const aEntered = deferred ( ) ;
155215 const releaseBarrier = deferred ( ) ;
156216 const aReleasePaused = deferred ( ) ;
157217 const holdB = deferred ( ) ;
158218 const bEntered = deferred ( ) ;
159219
160- const ownerA = withRawTranscriptSessionMarkerLock (
161- markerPath ,
162- async ( ) => {
163- aEntered . resolve ( ) ;
164- await holdA . promise ;
165- } ,
166- {
220+ const ownerA = commitCheckpoint ( mainPath , root , 0 , {
221+ lock : {
167222 onAfterReleaseTokensUnlinkedBeforeRmdir : async ( ) => {
168223 aReleasePaused . resolve ( ) ;
169224 await releaseBarrier . promise ;
170225 } ,
171- }
172- ) ;
226+ } ,
227+ onLocked : async ( ) => {
228+ aEntered . resolve ( ) ;
229+ await holdA . promise ;
230+ } ,
231+ } ) ;
173232 await aEntered . promise ;
174- const ownerB = withRawTranscriptSessionMarkerLock (
175- markerPath ,
176- async ( ) => {
233+ const ownerB = commitCheckpoint ( mainPath , root , 1 , {
234+ lock : {
235+ now : ( ) => Date . now ( ) + STALE_MS + 5_000 ,
236+ isProcessAlive : ( ) => false ,
237+ } ,
238+ onLocked : async ( ) => {
177239 bEntered . resolve ( ) ;
178240 await holdB . promise ;
179241 } ,
180- {
181- now : ( ) => Date . now ( ) + STALE_MS + 5_000 ,
182- isProcessAlive : ( ) => false ,
183- }
184- ) ;
242+ } ) ;
185243 await bEntered . promise ;
186244
187245 holdA . resolve ( ) ;
188246 await aReleasePaused . promise ;
189247 let interloperEntered = false ;
190- const interloperError = await withRawTranscriptSessionMarkerLock (
191- markerPath ,
192- async ( ) => {
248+ const interloperError = await commitCheckpoint ( mainPath , root , 2 , {
249+ lock : FAST_ACQUIRE ,
250+ onLocked : ( ) => {
193251 interloperEntered = true ;
194252 } ,
195- FAST_ACQUIRE
196- ) . then (
253+ } ) . then (
197254 ( ) => null ,
198255 ( error : unknown ) => error
199256 ) ;
@@ -213,32 +270,29 @@ describe('raw transcript session lock three-party schedules', () => {
213270 } ) ;
214271
215272 it ( 'T3 rejects interloper C while stale owner A reclamation is in flight' , async ( ) => {
216- const { markerPath , lockPath } = await fixture ( 't3-reclaim-interloper' ) ;
273+ const { root , mainPath , lockPath } = await fixture ( 't3-reclaim-interloper' ) ;
217274 await plantStaleToken ( lockPath ) ;
218275 const reclaimBarrier = deferred ( ) ;
219276 const reclaimerPaused = deferred ( ) ;
220277
221- const reclaimer = withRawTranscriptSessionMarkerLock (
222- markerPath ,
223- async ( ) => undefined ,
224- {
278+ const reclaimer = commitCheckpoint ( mainPath , root , 0 , {
279+ lock : {
225280 isProcessAlive : ( ) => false ,
226281 onAfterExpiredTokensUnlinkedBeforeRmdir : async ( ) => {
227282 reclaimerPaused . resolve ( ) ;
228283 await reclaimBarrier . promise ;
229284 } ,
230- }
231- ) ;
285+ } ,
286+ } ) ;
232287 await reclaimerPaused . promise ;
233288
234289 let interloperEntered = false ;
235- const interloperError = await withRawTranscriptSessionMarkerLock (
236- markerPath ,
237- async ( ) => {
290+ const interloperError = await commitCheckpoint ( mainPath , root , 0 , {
291+ lock : FAST_ACQUIRE ,
292+ onLocked : ( ) => {
238293 interloperEntered = true ;
239294 } ,
240- FAST_ACQUIRE
241- ) . then (
295+ } ) . then (
242296 ( ) => null ,
243297 ( error : unknown ) => error
244298 ) ;
0 commit comments