11import { afterEach , describe , expect , it } from 'bun:test'
2+ import { randomUUID } from 'node:crypto'
23import { mkdtempSync , mkdirSync , rmSync , writeFileSync } from 'node:fs'
34import { tmpdir } from 'node:os'
45import { join } from 'node:path'
56import { Hono } from 'hono'
67import { AGENT_MESSAGE_PAYLOAD_TYPE } from '@hapi/protocol'
78import { Store } from '../../store'
8- import type { SyncEngine } from '../../sync/syncEngine'
9+ import type { Machine , SyncEngine } from '../../sync/syncEngine'
910import type { WebAppEnv } from '../middleware/auth'
1011import { createCodexDesktopRoutes , importSelectedCodexSessions } from './codexDesktop'
1112
1213const originalCodexHome = process . env . CODEX_HOME
1314
14- function createTranscript ( codexHome : string , sessionId : string ) : void {
15+ function createTranscript ( codexHome : string , sessionId : string , cwd = 'C:\\work\\project' ) : void {
1516 const sessionDir = join ( codexHome , 'sessions' , '2026' , '06' , '04' )
1617 mkdirSync ( sessionDir , { recursive : true } )
1718 const transcriptPath = join ( sessionDir , `rollout-${ sessionId } .jsonl` )
@@ -20,7 +21,7 @@ function createTranscript(codexHome: string, sessionId: string): void {
2021 type : 'session_meta' ,
2122 payload : {
2223 id : sessionId ,
23- cwd : 'C:\\work\\project' ,
24+ cwd,
2425 originator : 'codex_cli_rs' ,
2526 cli_version : '0.0.0-test'
2627 }
@@ -45,6 +46,50 @@ function createTranscript(codexHome: string, sessionId: string): void {
4546 writeFileSync ( transcriptPath , `${ lines . map ( ( line ) => JSON . stringify ( line ) ) . join ( '\n' ) } \n` , 'utf-8' )
4647}
4748
49+ function createMachine ( id : string , workspaceRoots : string [ ] , namespace = 'default' ) : Machine {
50+ return {
51+ id,
52+ namespace,
53+ seq : 0 ,
54+ createdAt : 0 ,
55+ updatedAt : 0 ,
56+ active : true ,
57+ activeAt : 0 ,
58+ metadata : {
59+ host : id ,
60+ platform : 'linux' ,
61+ happyCliVersion : '0.0.0-test' ,
62+ workspaceRoots
63+ } ,
64+ metadataVersion : 1 ,
65+ runnerState : null ,
66+ runnerStateVersion : 1
67+ }
68+ }
69+
70+ function createImportSyncEngine ( store : Store , machines : Machine [ ] ) : SyncEngine {
71+ return {
72+ getOnlineMachinesByNamespace : ( namespace : string ) => machines . filter ( ( machine ) => (
73+ machine . namespace === namespace && machine . active
74+ ) ) ,
75+ getSessionsByNamespace : ( namespace : string ) => (
76+ store . sessions . getSessionsByNamespace ( namespace ) as unknown as ReturnType < SyncEngine [ 'getSessionsByNamespace' ] >
77+ ) ,
78+ getOrCreateSession : (
79+ tag : string ,
80+ metadata : unknown ,
81+ agentState : unknown ,
82+ namespace : string
83+ ) => (
84+ store . sessions . getOrCreateSession ( tag , metadata , agentState , namespace ) as unknown as ReturnType < SyncEngine [ 'getOrCreateSession' ] >
85+ ) ,
86+ handleRealtimeEvent : ( ) => { } ,
87+ recordSessionActivity : ( sessionId : string , updatedAt : number ) => {
88+ store . sessions . touchSessionUpdatedAt ( sessionId , updatedAt , 'default' )
89+ }
90+ } as unknown as SyncEngine
91+ }
92+
4893function createRoutesApp ( namespace : string ) : Hono < WebAppEnv > {
4994 const app = new Hono < WebAppEnv > ( )
5095 app . use ( '*' , async ( c , next ) => {
@@ -118,6 +163,138 @@ describe('Codex Desktop import routes', () => {
118163 }
119164 } )
120165
166+ it ( 'binds imported transcripts to the unique online machine that owns the cwd' , async ( ) => {
167+ const codexHome = mkdtempSync ( join ( tmpdir ( ) , 'hapi-codex-home-machine-test-' ) )
168+ const store = new Store ( ':memory:' )
169+ const codexSessionId = '22222222-2222-4222-8222-222222222222'
170+ process . env . CODEX_HOME = codexHome
171+
172+ try {
173+ createTranscript ( codexHome , codexSessionId , '/home/user/workspace/project' )
174+ const engine = createImportSyncEngine ( store , [
175+ createMachine ( 'machine-1' , [ '/home/user/workspace' ] ) ,
176+ createMachine ( 'machine-2' , [ '/other/workspace' ] )
177+ ] )
178+
179+ const result = await importSelectedCodexSessions ( {
180+ codexSessionIds : [ codexSessionId ] ,
181+ store,
182+ namespace : 'default' ,
183+ getSyncEngine : ( ) => engine
184+ } )
185+
186+ expect ( result . success ) . toBe ( true )
187+ const session = store . sessions . getSessionsByNamespace ( 'default' ) [ 0 ]
188+ expect ( session . metadata ) . toMatchObject ( {
189+ path : '/home/user/workspace/project' ,
190+ machineId : 'machine-1'
191+ } )
192+ } finally {
193+ store . close ( )
194+ rmSync ( codexHome , { recursive : true , force : true } )
195+ }
196+ } )
197+
198+ it ( 'does not bind imported transcripts when multiple online machines own the cwd' , async ( ) => {
199+ const codexHome = mkdtempSync ( join ( tmpdir ( ) , 'hapi-codex-home-machine-ambiguous-test-' ) )
200+ const store = new Store ( ':memory:' )
201+ const codexSessionId = '33333333-3333-4333-8333-333333333333'
202+ process . env . CODEX_HOME = codexHome
203+
204+ try {
205+ createTranscript ( codexHome , codexSessionId , '/home/user/workspace/project' )
206+ const engine = createImportSyncEngine ( store , [
207+ createMachine ( 'machine-1' , [ '/home/user/workspace' ] ) ,
208+ createMachine ( 'machine-2' , [ '/home/user/workspace/project' ] )
209+ ] )
210+
211+ const result = await importSelectedCodexSessions ( {
212+ codexSessionIds : [ codexSessionId ] ,
213+ store,
214+ namespace : 'default' ,
215+ getSyncEngine : ( ) => engine
216+ } )
217+
218+ expect ( result . success ) . toBe ( true )
219+ const session = store . sessions . getSessionsByNamespace ( 'default' ) [ 0 ]
220+ expect ( session . metadata ) . toMatchObject ( {
221+ path : '/home/user/workspace/project'
222+ } )
223+ expect ( session . metadata ) . not . toHaveProperty ( 'machineId' )
224+ } finally {
225+ store . close ( )
226+ rmSync ( codexHome , { recursive : true , force : true } )
227+ }
228+ } )
229+
230+ it ( 'does not bind imported transcripts when no online machine owns the cwd' , async ( ) => {
231+ const codexHome = mkdtempSync ( join ( tmpdir ( ) , 'hapi-codex-home-machine-miss-test-' ) )
232+ const store = new Store ( ':memory:' )
233+ const codexSessionId = '44444444-4444-4444-8444-444444444444'
234+ process . env . CODEX_HOME = codexHome
235+
236+ try {
237+ createTranscript ( codexHome , codexSessionId , '/home/user/workspace/project' )
238+ const engine = createImportSyncEngine ( store , [
239+ createMachine ( 'machine-1' , [ '/home/user/other' ] )
240+ ] )
241+
242+ const result = await importSelectedCodexSessions ( {
243+ codexSessionIds : [ codexSessionId ] ,
244+ store,
245+ namespace : 'default' ,
246+ getSyncEngine : ( ) => engine
247+ } )
248+
249+ expect ( result . success ) . toBe ( true )
250+ const session = store . sessions . getSessionsByNamespace ( 'default' ) [ 0 ]
251+ expect ( session . metadata ) . toMatchObject ( {
252+ path : '/home/user/workspace/project'
253+ } )
254+ expect ( session . metadata ) . not . toHaveProperty ( 'machineId' )
255+ } finally {
256+ store . close ( )
257+ rmSync ( codexHome , { recursive : true , force : true } )
258+ }
259+ } )
260+
261+ it ( 'keeps an existing machineId when updating an imported transcript' , async ( ) => {
262+ const codexHome = mkdtempSync ( join ( tmpdir ( ) , 'hapi-codex-home-machine-existing-test-' ) )
263+ const store = new Store ( ':memory:' )
264+ const codexSessionId = '55555555-5555-4555-8555-555555555555'
265+ process . env . CODEX_HOME = codexHome
266+
267+ try {
268+ createTranscript ( codexHome , codexSessionId , '/home/user/workspace/project' )
269+ store . sessions . getOrCreateSession ( randomUUID ( ) , {
270+ path : '/home/user/workspace/project' ,
271+ flavor : 'codex' ,
272+ codexSessionId,
273+ machineId : 'machine-existing'
274+ } , { } , 'default' )
275+ const engine = createImportSyncEngine ( store , [
276+ createMachine ( 'machine-new' , [ '/home/user/workspace' ] )
277+ ] )
278+
279+ const result = await importSelectedCodexSessions ( {
280+ codexSessionIds : [ codexSessionId ] ,
281+ store,
282+ namespace : 'default' ,
283+ getSyncEngine : ( ) => engine
284+ } )
285+
286+ expect ( result . success ) . toBe ( true )
287+ const session = store . sessions . getSessionsByNamespace ( 'default' ) [ 0 ]
288+ expect ( session . metadata ) . toMatchObject ( {
289+ path : '/home/user/workspace/project' ,
290+ machineId : 'machine-existing'
291+ } )
292+ } finally {
293+ store . close ( )
294+ rmSync ( codexHome , { recursive : true , force : true } )
295+ }
296+ } )
297+
121298 it ( 'rejects Codex transcript endpoints outside the default namespace' , async ( ) => {
122299 const app = createRoutesApp ( 'team-a' )
123300 const response = await app . request ( '/api/codex/sessions' )
0 commit comments