@@ -12,6 +12,7 @@ import {
1212 mkdirSync ,
1313 rmSync ,
1414 realpathSync ,
15+ writeFileSync ,
1516} from "node:fs" ;
1617import { tmpdir } from "node:os" ;
1718import { execFileSync } from "node:child_process" ;
@@ -25,13 +26,19 @@ export interface ServerInstance {
2526 port : number ;
2627 baseUrl : string ;
2728 token : string ;
29+ ocrDir : string ;
2830 process : ChildProcess ;
2931 cleanup : ( ) => Promise < void > ;
3032}
3133
3234let portCounter = 14_000 + Math . floor ( Math . random ( ) * 1000 ) ;
3335
34- export async function startTestServer ( ) : Promise < ServerInstance > {
36+ export interface StartOptions {
37+ /** Write a stale port file before starting the server (for testing port file cleanup). */
38+ stalePort ?: number ;
39+ }
40+
41+ export async function startTestServer ( opts ?: StartOptions ) : Promise < ServerInstance > {
3542 const port = portCounter ++ ;
3643 const tmpDir = realpathSync (
3744 mkdtempSync ( resolve ( tmpdir ( ) , "ocr-dash-e2e-" ) ) ,
@@ -47,6 +54,14 @@ export async function startTestServer(): Promise<ServerInstance> {
4754 mkdirSync ( resolve ( tmpDir , ".ocr" , "sessions" ) , { recursive : true } ) ;
4855 mkdirSync ( resolve ( tmpDir , ".ocr" , "data" ) , { recursive : true } ) ;
4956
57+ // Optionally write a stale port file to test cleanup behavior
58+ if ( opts ?. stalePort ) {
59+ writeFileSync (
60+ resolve ( tmpDir , ".ocr" , "data" , "server-port" ) ,
61+ String ( opts . stalePort ) ,
62+ ) ;
63+ }
64+
5065 const child = fork ( SERVER_ENTRY , [ ] , {
5166 cwd : tmpDir ,
5267 env : {
@@ -71,6 +86,7 @@ export async function startTestServer(): Promise<ServerInstance> {
7186 port,
7287 baseUrl,
7388 token,
89+ ocrDir : resolve ( tmpDir , ".ocr" ) ,
7490 process : child ,
7591 cleanup : async ( ) => {
7692 child . kill ( ) ;
@@ -84,6 +100,68 @@ export async function startTestServer(): Promise<ServerInstance> {
84100 } ;
85101}
86102
103+ /**
104+ * Start a server but return immediately — before waiting for health.
105+ * Used to inspect the port file during the startup window.
106+ */
107+ export function startServerEarly ( opts ?: StartOptions ) : {
108+ portFilePath : string ;
109+ child : ChildProcess ;
110+ port : number ;
111+ tmpDir : string ;
112+ waitForHealth : ( ) => Promise < void > ;
113+ cleanup : ( ) => Promise < void > ;
114+ } {
115+ const port = portCounter ++ ;
116+ const tmpDir = realpathSync (
117+ mkdtempSync ( resolve ( tmpdir ( ) , "ocr-dash-e2e-" ) ) ,
118+ ) ;
119+
120+ execFileSync ( "git" , [ "init" ] , { cwd : tmpDir , stdio : "ignore" } ) ;
121+ execFileSync ( "git" , [ "commit" , "--allow-empty" , "-m" , "init" ] , {
122+ cwd : tmpDir ,
123+ stdio : "ignore" ,
124+ } ) ;
125+ mkdirSync ( resolve ( tmpDir , ".ocr" , "skills" ) , { recursive : true } ) ;
126+ mkdirSync ( resolve ( tmpDir , ".ocr" , "sessions" ) , { recursive : true } ) ;
127+ mkdirSync ( resolve ( tmpDir , ".ocr" , "data" ) , { recursive : true } ) ;
128+
129+ const portFilePath = resolve ( tmpDir , ".ocr" , "data" , "server-port" ) ;
130+
131+ if ( opts ?. stalePort ) {
132+ writeFileSync ( portFilePath , String ( opts . stalePort ) ) ;
133+ }
134+
135+ const child = fork ( SERVER_ENTRY , [ ] , {
136+ cwd : tmpDir ,
137+ env : {
138+ ...process . env ,
139+ PORT : String ( port ) ,
140+ NODE_ENV : "test" ,
141+ NO_COLOR : "1" ,
142+ } ,
143+ stdio : "pipe" ,
144+ } ) ;
145+
146+ const baseUrl = `http://127.0.0.1:${ port } ` ;
147+
148+ return {
149+ portFilePath,
150+ child,
151+ port,
152+ tmpDir,
153+ waitForHealth : ( ) => waitForHealth ( baseUrl , 15_000 ) ,
154+ cleanup : async ( ) => {
155+ child . kill ( ) ;
156+ await new Promise < void > ( ( r ) => {
157+ child . on ( "exit" , ( ) => r ( ) ) ;
158+ setTimeout ( ( ) => r ( ) , 5_000 ) ;
159+ } ) ;
160+ rmSync ( tmpDir , { recursive : true , force : true } ) ;
161+ } ,
162+ } ;
163+ }
164+
87165async function waitForHealth (
88166 baseUrl : string ,
89167 timeoutMs : number ,
0 commit comments