@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
55import { ConfigMutationLockError , loadConfig , saveConfig , withConfigMutationLockSync } from "../src/config" ;
66import { CodexCredentialRefreshLockTimeoutError , getCodexAccountCredential , saveCodexAccountCredential } from "../src/codex/account-store" ;
77import type { OcxConfig } from "../src/types" ;
8+ import { ManagementRequest , managementHeaders } from "./helpers/management-auth" ;
89
910let testRoot = "" ;
1011let previousOpencodexHome : string | undefined ;
@@ -135,3 +136,48 @@ test("a throwing mutation releases the lock and leaves writers available", () =>
135136 expect ( ( ) => saveConfig ( config ( 50500 ) ) ) . not . toThrow ( ) ;
136137 expect ( loadConfig ( ) . port ) . toBe ( 50500 ) ;
137138} ) ;
139+
140+ test ( "management API maps config mutation lock contention to retryable 503" , async ( ) => {
141+ saveConfig ( config ( ) ) ;
142+ const readyPath = join ( testRoot , "mgmt-holder-ready" ) ;
143+ const releasePath = join ( testRoot , "mgmt-holder-release" ) ;
144+ const configModuleUrl = pathToFileURL ( join ( import . meta. dir , "../src/config.ts" ) ) . href ;
145+ const childSource = `
146+ import { existsSync, writeFileSync } from "node:fs";
147+ import { withConfigMutationLockSync } from ${ JSON . stringify ( configModuleUrl ) } ;
148+ withConfigMutationLockSync(() => {
149+ writeFileSync(${ JSON . stringify ( readyPath ) } , "ready");
150+ while (!existsSync(${ JSON . stringify ( releasePath ) } )) Bun.sleepSync(10);
151+ });
152+ ` ;
153+ const child = Bun . spawn ( [ process . execPath , "-e" , childSource ] , {
154+ cwd : join ( import . meta. dir , ".." ) ,
155+ env : { ...process . env , OPENCODEX_HOME : testRoot } ,
156+ stdin : "ignore" ,
157+ stdout : "pipe" ,
158+ stderr : "pipe" ,
159+ } ) ;
160+
161+ try {
162+ await waitForPath ( readyPath ) ;
163+ const { handleManagementAPI } = await import ( "../src/server/management-api" ) ;
164+ const url = new URL ( "http://localhost/api/codex-auth/auto-switch" ) ;
165+ const response = await handleManagementAPI (
166+ new ManagementRequest ( url , {
167+ method : "PUT" ,
168+ headers : managementHeaders ( { "content-type" : "application/json" } ) ,
169+ body : JSON . stringify ( { threshold : 50 } ) ,
170+ } ) ,
171+ url ,
172+ config ( ) ,
173+ ) ;
174+ expect ( response ?. status ) . toBe ( 503 ) ;
175+ expect ( await response ! . json ( ) ) . toMatchObject ( {
176+ error : "Configuration is busy; retry shortly" ,
177+ code : "CONFIG_MUTATION_LOCK_UNAVAILABLE" ,
178+ } ) ;
179+ } finally {
180+ writeFileSync ( releasePath , "release" ) ;
181+ expect ( await waitForOwnedChild ( child ) ) . toBe ( 0 ) ;
182+ }
183+ } ) ;
0 commit comments