11import { afterEach , describe , expect , it , vi } from "vitest" ;
2- import { mkdtempSync , readFileSync , rmSync , existsSync , writeFileSync , readdirSync , mkdirSync } from "node:fs" ;
2+ import {
3+ existsSync ,
4+ mkdirSync ,
5+ mkdtempSync ,
6+ readdirSync ,
7+ rmSync ,
8+ writeFileSync ,
9+ readFileSync ,
10+ } from "node:fs" ;
311import { tmpdir } from "node:os" ;
412import path from "node:path" ;
5- import { spawnSync , execFile } from "node:child_process" ;
13+ import { execFile , spawnSync } from "node:child_process" ;
614import { promisify } from "node:util" ;
715import {
816 FILE_RETRY_BASE_DELAY_MS ,
917 FILE_RETRY_MAX_ATTEMPTS ,
10- normalizePluginList ,
18+ PLUGIN_MARKETPLACE ,
19+ PLUGIN_NAME ,
20+ PLUGIN_VERSION ,
21+ makePluginKey ,
22+ mergePluginConfigToml ,
1123 resolveInstallPaths ,
1224 withFileOperationRetry ,
1325} from "../scripts/install-codex-auth-utils.js" ;
@@ -34,113 +46,126 @@ function retryableError(code: string): Error & { code: string } {
3446}
3547
3648describe ( "install-codex-auth script" , ( ) => {
37- it ( "uses lowercase config template filenames" , ( ) => {
38- const content = readFileSync ( scriptPath , "utf8" ) ;
39- expect ( content ) . toContain ( '"codex-legacy.json"' ) ;
40- expect ( content ) . toContain ( '"codex-modern.json"' ) ;
41- expect ( content ) . not . toContain ( '"Codex-legacy.json"' ) ;
42- expect ( content ) . not . toContain ( '"Codex-modern.json"' ) ;
43- } ) ;
44-
45- it ( "normalizes plugin list with empty, duplicate, and non-string entries" , ( ) => {
46- expect ( normalizePluginList ( undefined ) ) . toEqual ( [ "codex-multi-auth" ] ) ;
47- expect ( normalizePluginList ( [ "codex-multi-auth" , "a" , "a" , 123 , null ] ) ) . toEqual ( [
48- "a" ,
49- 123 ,
50- "codex-multi-auth" ,
51- ] ) ;
52- expect ( normalizePluginList ( [ "codex-multi-auth@1.0.0" , "b" ] ) ) . toEqual ( [
53- "b" ,
54- "codex-multi-auth" ,
55- ] ) ;
56- } ) ;
57-
58- it ( "uses APPDATA/LOCALAPPDATA on windows path resolution" , ( ) => {
49+ it ( "resolves official Codex paths from CODEX_HOME" , ( ) => {
5950 const paths = resolveInstallPaths (
60- "win32 " ,
51+ "linux " ,
6152 {
62- APPDATA : "C:\\Users\\test\\AppData\\Roaming" ,
63- LOCALAPPDATA : "C:\\Users\\test\\AppData\\Local" ,
53+ CODEX_HOME : "/tmp/codex-home" ,
6454 } ,
65- "C:\\Users\\test" ,
66- ) ;
67- expect ( paths . configPath ) . toBe (
68- path . join ( "C:\\Users\\test\\AppData\\Roaming" , "Codex" , "Codex.json" ) ,
55+ "/home/test" ,
6956 ) ;
70- expect ( paths . cacheNodeModules ) . toBe (
57+ expect ( paths . codexHomeDir ) . toBe ( "/tmp/codex-home" ) ;
58+ expect ( paths . configPath ) . toBe ( path . join ( "/tmp/codex-home" , "config.toml" ) ) ;
59+ expect ( paths . pluginInstallDir ) . toBe (
7160 path . join (
72- "C:\\Users\\test\\AppData\\Local" ,
73- "Codex" ,
74- "node_modules" ,
75- "codex-multi-auth" ,
61+ "/tmp/codex-home" ,
62+ "plugins" ,
63+ "cache" ,
64+ PLUGIN_MARKETPLACE ,
65+ PLUGIN_NAME ,
66+ PLUGIN_VERSION ,
7667 ) ,
7768 ) ;
7869 } ) ;
7970
80- it ( "creates distinct backup files when installer runs concurrently" , async ( ) => {
81- const home = mkdtempSync ( path . join ( tmpdir ( ) , "codex-install-race-" ) ) ;
71+ it ( "respects CODEX_CLI_CONFIG_PATH override" , ( ) => {
72+ const paths = resolveInstallPaths (
73+ "linux" ,
74+ {
75+ CODEX_HOME : "/tmp/codex-home" ,
76+ CODEX_CLI_CONFIG_PATH : "/tmp/custom/config.toml" ,
77+ } ,
78+ "/home/test" ,
79+ ) ;
80+ expect ( paths . configPath ) . toBe ( "/tmp/custom/config.toml" ) ;
81+ expect ( paths . configDir ) . toBe ( path . dirname ( "/tmp/custom/config.toml" ) ) ;
82+ } ) ;
83+
84+ it ( "merges plugin settings into config.toml" , ( ) => {
85+ const merged = mergePluginConfigToml (
86+ [ "[features]" , "plugins = false" , "" , "[plugins.\"codex-multi-auth@ndycode\"]" , "enabled = false" , "" ] . join ( "\n" ) ,
87+ makePluginKey ( ) ,
88+ ) ;
89+ expect ( merged ) . toContain ( "[features]" ) ;
90+ expect ( merged ) . toContain ( "plugins = true" ) ;
91+ expect ( merged ) . toContain ( '[plugins."codex-multi-auth@ndycode"]' ) ;
92+ expect ( merged ) . toContain ( "enabled = true" ) ;
93+ } ) ;
94+
95+ it ( "dry-run does not create config or plugin cache" , ( ) => {
96+ const home = mkdtempSync ( path . join ( tmpdir ( ) , "codex-plugin-dryrun-" ) ) ;
8297 tempRoots . push ( home ) ;
83- const appData = path . join ( home , "AppData" , "Roaming" ) ;
84- const localAppData = path . join ( home , "AppData" , "Local" ) ;
98+ const codexHome = path . join ( home , ".codex" ) ;
8599 const env = {
86100 ...process . env ,
87101 HOME : home ,
88102 USERPROFILE : home ,
89- APPDATA : appData ,
90- LOCALAPPDATA : localAppData ,
103+ CODEX_HOME : codexHome ,
91104 } ;
92- const configDir = path . join ( appData , "Codex" ) ;
93- const configPath = path . join ( configDir , "Codex.json" ) ;
94- const initialConfig = JSON . stringify ( { plugin : [ "existing-plugin" ] } , null , 2 ) ;
95-
96- mkdirSync ( configDir , { recursive : true } ) ;
97- writeFileSync ( configPath , `${ initialConfig } \n` , "utf8" ) ;
98-
99- const [ first , second ] = await Promise . all ( [
100- execFileAsync ( process . execPath , [ scriptPath , "--modern" , "--no-cache-clear" ] , {
101- env,
102- windowsHide : true ,
103- } ) ,
104- execFileAsync ( process . execPath , [ scriptPath , "--legacy" , "--no-cache-clear" ] , {
105- env,
106- windowsHide : true ,
107- } ) ,
108- ] ) ;
109-
110- expect ( first . stderr ) . toBe ( "" ) ;
111- expect ( second . stderr ) . toBe ( "" ) ;
112- expect ( first . stdout ) . toContain ( "Backup created" ) ;
113- expect ( second . stdout ) . toContain ( "Backup created" ) ;
114- const backups = readdirSync ( configDir ) . filter ( ( entry ) =>
115- entry . startsWith ( "Codex.json.bak-" ) ,
116- ) ;
117- expect ( new Set ( backups ) . size ) . toBe ( backups . length ) ;
118- expect ( backups . length ) . toBeGreaterThanOrEqual ( 2 ) ;
105+
106+ const result = spawnSync ( process . execPath , [ scriptPath , "--dry-run" , "--modern" ] , {
107+ env,
108+ encoding : "utf8" ,
109+ windowsHide : true ,
110+ } ) ;
111+
112+ expect ( result . status ) . toBe ( 0 ) ;
113+ expect ( `${ result . stdout } \n${ result . stderr } ` ) . toContain ( "[dry-run]" ) ;
114+ expect ( existsSync ( path . join ( codexHome , "config.toml" ) ) ) . toBe ( false ) ;
115+ expect ( existsSync ( path . join ( codexHome , "plugins" , "cache" ) ) ) . toBe ( false ) ;
119116 } ) ;
120117
121- it ( "dry-run does not create global config on disk" , ( ) => {
122- const home = mkdtempSync ( path . join ( tmpdir ( ) , "codex-install-dryrun -" ) ) ;
118+ it ( "installs plugin cache and updates config.toml" , async ( ) => {
119+ const home = mkdtempSync ( path . join ( tmpdir ( ) , "codex-plugin-install -" ) ) ;
123120 tempRoots . push ( home ) ;
124- const appData = path . join ( home , "AppData" , "Roaming ") ;
125- const localAppData = path . join ( home , "AppData" , "Local ") ;
121+ const codexHome = path . join ( home , ".codex " ) ;
122+ const configPath = path . join ( codexHome , "config.toml " ) ;
126123 const env = {
127124 ...process . env ,
128125 HOME : home ,
129126 USERPROFILE : home ,
130- APPDATA : appData ,
131- LOCALAPPDATA : localAppData ,
127+ CODEX_HOME : codexHome ,
132128 } ;
133129
134- const result = spawnSync ( process . execPath , [ scriptPath , "--dry-run" , "--modern" ] , {
130+ mkdirSync ( path . dirname ( configPath ) , { recursive : true } ) ;
131+ writeFileSync (
132+ configPath ,
133+ [ "model = \"gpt-5-codex\"" , "" , "[features]" , "shell_tool = true" , "" ] . join ( "\n" ) ,
134+ "utf8" ,
135+ ) ;
136+
137+ const result = await execFileAsync ( process . execPath , [ scriptPath , "--no-cache-clear" ] , {
135138 env,
136- encoding : "utf8" ,
137139 windowsHide : true ,
138140 } ) ;
139141
140- expect ( result . status ) . toBe ( 0 ) ;
141- expect ( `${ result . stdout } \n${ result . stderr } ` ) . toContain ( "[dry-run]" ) ;
142- const configPath = path . join ( appData , "Codex" , "Codex.json" ) ;
143- expect ( existsSync ( configPath ) ) . toBe ( false ) ;
142+ expect ( result . stderr ) . toBe ( "" ) ;
143+ expect ( result . stdout ) . toContain ( "Installed plugin cache" ) ;
144+ expect ( result . stdout ) . toContain ( "Backup created" ) ;
145+
146+ const config = readFileSync ( configPath , "utf8" ) ;
147+ expect ( config ) . toContain ( "model = \"gpt-5-codex\"" ) ;
148+ expect ( config ) . toContain ( "[features]" ) ;
149+ expect ( config ) . toContain ( "plugins = true" ) ;
150+ expect ( config ) . toContain ( '[plugins."codex-multi-auth@ndycode"]' ) ;
151+ expect ( config ) . toContain ( "enabled = true" ) ;
152+
153+ const pluginManifestPath = path . join (
154+ codexHome ,
155+ "plugins" ,
156+ "cache" ,
157+ PLUGIN_MARKETPLACE ,
158+ PLUGIN_NAME ,
159+ PLUGIN_VERSION ,
160+ ".codex-plugin" ,
161+ "plugin.json" ,
162+ ) ;
163+ expect ( existsSync ( pluginManifestPath ) ) . toBe ( true ) ;
164+
165+ const backups = readdirSync ( codexHome ) . filter ( ( entry ) =>
166+ entry . startsWith ( "config.toml.bak-" )
167+ ) ;
168+ expect ( backups . length ) . toBe ( 1 ) ;
144169 } ) ;
145170
146171 it ( "retries transient file-operation errors and eventually succeeds" , async ( ) => {
0 commit comments