1- import { describe , expect , test } from "bun:test" ;
2- import { codexAutoStartEnabled , getDefaultConfig } from "../src/config" ;
1+ import { afterEach , beforeEach , describe , expect , spyOn , test } from "bun:test" ;
2+ import { existsSync , mkdtempSync , readdirSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
3+ import { tmpdir } from "node:os" ;
4+ import { join } from "node:path" ;
5+ import { codexAutoStartEnabled , getConfigPath , getDefaultConfig , loadConfig } from "../src/config" ;
6+
7+ let testDir = "" ;
8+
9+ beforeEach ( ( ) => {
10+ testDir = mkdtempSync ( join ( tmpdir ( ) , "ocx-config-" ) ) ;
11+ process . env . OPENCODEX_HOME = testDir ;
12+ } ) ;
13+
14+ afterEach ( ( ) => {
15+ delete process . env . OPENCODEX_HOME ;
16+ if ( testDir && existsSync ( testDir ) ) rmSync ( testDir , { recursive : true , force : true } ) ;
17+ testDir = "" ;
18+ } ) ;
19+
20+ function backupNames ( ) : string [ ] {
21+ return readdirSync ( testDir ) . filter ( name => name . startsWith ( "config.json.invalid-" ) ) ;
22+ }
23+
24+ function writeConfig ( content : unknown ) : void {
25+ writeFileSync (
26+ getConfigPath ( ) ,
27+ typeof content === "string" ? content : JSON . stringify ( content ) ,
28+ "utf-8" ,
29+ ) ;
30+ }
331
432describe ( "opencodex config defaults" , ( ) => {
533 test ( "Codex autostart is enabled by default" , ( ) => {
@@ -11,4 +39,95 @@ describe("opencodex config defaults", () => {
1139 expect ( codexAutoStartEnabled ( { codexAutoStart : false } ) ) . toBe ( false ) ;
1240 expect ( codexAutoStartEnabled ( { codexAutoStart : true } ) ) . toBe ( true ) ;
1341 } ) ;
42+
43+ test ( "loads valid config from OPENCODEX_HOME" , ( ) => {
44+ writeConfig ( {
45+ port : 12345 ,
46+ providers : {
47+ custom : { adapter : "openai-chat" , baseUrl : "https://example.test/v1" } ,
48+ } ,
49+ defaultProvider : "custom" ,
50+ codexAutoStart : false ,
51+ } ) ;
52+
53+ expect ( loadConfig ( ) ) . toMatchObject ( {
54+ port : 12345 ,
55+ defaultProvider : "custom" ,
56+ providers : {
57+ custom : { adapter : "openai-chat" , baseUrl : "https://example.test/v1" } ,
58+ } ,
59+ codexAutoStart : false ,
60+ } ) ;
61+ } ) ;
62+
63+ test ( "backs up invalid JSON config before falling back to defaults" , ( ) => {
64+ writeConfig ( "{ invalid json" ) ;
65+ const errorSpy = spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
66+
67+ try {
68+ const loaded = loadConfig ( ) ;
69+
70+ expect ( loaded ) . toEqual ( getDefaultConfig ( ) ) ;
71+ const backups = backupNames ( ) ;
72+ expect ( backups ) . toHaveLength ( 1 ) ;
73+ expect ( readFileSync ( join ( testDir , backups [ 0 ] ) , "utf-8" ) ) . toBe ( "{ invalid json" ) ;
74+ expect ( errorSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "Could not load opencodex config" ) ) ;
75+ } finally {
76+ errorSpy . mockRestore ( ) ;
77+ }
78+ } ) ;
79+
80+ test ( "backs up structurally invalid config before falling back to defaults" , ( ) => {
81+ writeConfig ( { port : 10100 } ) ;
82+ const errorSpy = spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
83+
84+ try {
85+ const loaded = loadConfig ( ) ;
86+
87+ expect ( loaded ) . toEqual ( getDefaultConfig ( ) ) ;
88+ const backups = backupNames ( ) ;
89+ expect ( backups ) . toHaveLength ( 1 ) ;
90+ expect ( JSON . parse ( readFileSync ( join ( testDir , backups [ 0 ] ) , "utf-8" ) ) ) . toEqual ( { port : 10100 } ) ;
91+ expect ( errorSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "providers" ) ) ;
92+ } finally {
93+ errorSpy . mockRestore ( ) ;
94+ }
95+ } ) ;
96+
97+ test ( "backs up config when defaultProvider is absent from providers" , ( ) => {
98+ writeConfig ( {
99+ port : 10100 ,
100+ providers : {
101+ openai : { adapter : "openai-responses" , baseUrl : "https://chatgpt.com/backend-api/codex" } ,
102+ } ,
103+ defaultProvider : "missing" ,
104+ } ) ;
105+ const errorSpy = spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
106+
107+ try {
108+ const loaded = loadConfig ( ) ;
109+
110+ expect ( loaded ) . toEqual ( getDefaultConfig ( ) ) ;
111+ const backups = backupNames ( ) ;
112+ expect ( backups ) . toHaveLength ( 1 ) ;
113+ expect ( errorSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "defaultProvider must exist in providers" ) ) ;
114+ } finally {
115+ errorSpy . mockRestore ( ) ;
116+ }
117+ } ) ;
118+
119+ test ( "warns and backs up once per invalid config path" , ( ) => {
120+ writeConfig ( "{ invalid json" ) ;
121+ const errorSpy = spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
122+
123+ try {
124+ expect ( loadConfig ( ) ) . toEqual ( getDefaultConfig ( ) ) ;
125+ expect ( loadConfig ( ) ) . toEqual ( getDefaultConfig ( ) ) ;
126+
127+ expect ( backupNames ( ) ) . toHaveLength ( 1 ) ;
128+ expect ( errorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
129+ } finally {
130+ errorSpy . mockRestore ( ) ;
131+ }
132+ } ) ;
14133} ) ;
0 commit comments