@@ -3,6 +3,14 @@ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promis
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
55
6+ type OpenAiTemplate = {
7+ provider : {
8+ openai : {
9+ models : Record < string , unknown > ;
10+ } ;
11+ } ;
12+ } ;
13+
614async function createTempHome ( ) {
715 return mkdtemp ( join ( tmpdir ( ) , "oc-chatgpt-install-" ) ) ;
816}
@@ -12,6 +20,7 @@ describe("install-opencode-codex-auth script", () => {
1220
1321 afterEach ( async ( ) => {
1422 vi . restoreAllMocks ( ) ;
23+ vi . doUnmock ( "node:fs/promises" ) ;
1524 if ( tempHome ) {
1625 await rm ( tempHome , { recursive : true , force : true } ) ;
1726 tempHome = null ;
@@ -84,7 +93,15 @@ describe("install-opencode-codex-auth script", () => {
8493 expect ( saved . customSetting ) . toBe ( true ) ;
8594 expect ( saved . plugin ) . toEqual ( [ "existing-plugin" , "oc-chatgpt-multi-auth" ] ) ;
8695 expect ( saved . provider . anthropic ) . toEqual ( { baseURL : "https://example.invalid" } ) ;
87- expect ( Object . keys ( saved . provider . openai . models ) ) . toHaveLength ( 43 ) ;
96+ const modernTemplate = JSON . parse (
97+ await readFile ( new URL ( "../config/opencode-modern.json" , import . meta. url ) , "utf-8" ) ,
98+ ) as OpenAiTemplate ;
99+ const legacyTemplate = JSON . parse (
100+ await readFile ( new URL ( "../config/opencode-legacy.json" , import . meta. url ) , "utf-8" ) ,
101+ ) as OpenAiTemplate ;
102+ const expectedCount = Object . keys ( modernTemplate . provider . openai . models ) . length
103+ + Object . keys ( legacyTemplate . provider . openai . models ) . length ;
104+ expect ( Object . keys ( saved . provider . openai . models ) ) . toHaveLength ( expectedCount ) ;
88105 expect ( saved . provider . openai . models [ "gpt-5.4" ] ) . toBeDefined ( ) ;
89106 expect ( saved . provider . openai . models [ "gpt-5.4-high" ] ) . toBeDefined ( ) ;
90107 const configEntries = await readdir ( configDir ) ;
@@ -148,4 +165,78 @@ describe("install-opencode-codex-auth script", () => {
148165 expect ( cachePackage . dependencies [ "oc-chatgpt-multi-auth" ] ) . toBeUndefined ( ) ;
149166 expect ( cachePackage . dependencies . other ) . toBe ( "^1.0.0" ) ;
150167 } ) ;
168+
169+ it ( "rejects full-mode merges when modern and legacy templates overlap" , async ( ) => {
170+ vi . resetModules ( ) ;
171+ const { __test } = await import ( "../scripts/install-opencode-codex-auth.js" ) ;
172+
173+ const modernTemplate = {
174+ provider : {
175+ openai : {
176+ models : {
177+ "gpt-5.4" : { name : "base" } ,
178+ } ,
179+ } ,
180+ } ,
181+ } ;
182+ const legacyTemplate = {
183+ provider : {
184+ openai : {
185+ models : {
186+ "gpt-5.4" : { name : "preset" } ,
187+ } ,
188+ } ,
189+ } ,
190+ } ;
191+
192+ expect ( ( ) => __test . mergeFullTemplate ( modernTemplate , legacyTemplate ) ) . toThrow (
193+ / F u l l c o n f i g t e m p l a t e c o l l i s i o n / ,
194+ ) ;
195+ } ) ;
196+
197+ it ( "retries backup copies after transient Windows lock errors" , async ( ) => {
198+ vi . resetModules ( ) ;
199+ tempHome = await createTempHome ( ) ;
200+ const sourcePath = join ( tempHome , "opencode.json" ) ;
201+ const copyFileMock = vi . fn ( )
202+ . mockRejectedValueOnce ( Object . assign ( new Error ( "busy" ) , { code : "EBUSY" } ) )
203+ . mockResolvedValue ( undefined ) ;
204+
205+ vi . doMock ( "node:fs/promises" , async ( ) => {
206+ const actual = await vi . importActual < typeof import ( "node:fs/promises" ) > ( "node:fs/promises" ) ;
207+ return {
208+ ...actual ,
209+ copyFile : copyFileMock ,
210+ } ;
211+ } ) ;
212+
213+ const { __test } = await import ( "../scripts/install-opencode-codex-auth.js" ) ;
214+ const backupPath = await __test . backupConfig ( sourcePath , false ) ;
215+
216+ expect ( copyFileMock ) . toHaveBeenCalledTimes ( 2 ) ;
217+ expect ( copyFileMock ) . toHaveBeenNthCalledWith ( 1 , sourcePath , backupPath ) ;
218+ expect ( copyFileMock ) . toHaveBeenNthCalledWith ( 2 , sourcePath , backupPath ) ;
219+ expect ( backupPath ) . toMatch ( / o p e n c o d e \. j s o n \. b a k - / ) ;
220+ } ) ;
221+
222+ it ( "retries atomic rename after transient Windows lock errors" , async ( ) => {
223+ vi . resetModules ( ) ;
224+ const renameMock = vi . fn ( )
225+ . mockRejectedValueOnce ( Object . assign ( new Error ( "locked" ) , { code : "EPERM" } ) )
226+ . mockResolvedValue ( undefined ) ;
227+
228+ vi . doMock ( "node:fs/promises" , async ( ) => {
229+ const actual = await vi . importActual < typeof import ( "node:fs/promises" ) > ( "node:fs/promises" ) ;
230+ return {
231+ ...actual ,
232+ rename : renameMock ,
233+ } ;
234+ } ) ;
235+
236+ const { __test } = await import ( "../scripts/install-opencode-codex-auth.js" ) ;
237+ await expect ( __test . renameWithWindowsRetry ( "from.tmp" , "to.json" ) ) . resolves . toBeUndefined ( ) ;
238+ expect ( renameMock ) . toHaveBeenCalledTimes ( 2 ) ;
239+ expect ( renameMock ) . toHaveBeenNthCalledWith ( 1 , "from.tmp" , "to.json" ) ;
240+ expect ( renameMock ) . toHaveBeenNthCalledWith ( 2 , "from.tmp" , "to.json" ) ;
241+ } ) ;
151242} ) ;
0 commit comments