11import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
22import { EventEmitter } from "node:events" ;
3+ import fs from "node:fs" ;
4+ import os from "node:os" ;
5+ import path from "node:path" ;
36
47const spawnMock = vi . fn ( ) ;
8+ const execFileSyncMock = vi . fn ( ) ;
59const getAllApiKeysMock = vi . fn ( ) ;
610
711/** Helper: create a fake ChildProcess that immediately emits close with the given result. */
@@ -36,6 +40,7 @@ vi.mock("node:child_process", async () => {
3640 return {
3741 ...actual ,
3842 spawn : ( ...args : unknown [ ] ) => spawnMock ( ...args ) ,
43+ execFileSync : ( ...args : unknown [ ] ) => execFileSyncMock ( ...args ) ,
3944 } ;
4045} ) ;
4146
@@ -47,9 +52,18 @@ vi.mock("./apiKeyStore", () => ({
4752let detectAllAuth : typeof import ( "./authDetector" ) . detectAllAuth ;
4853let detectCliAuthStatuses : typeof import ( "./authDetector" ) . detectCliAuthStatuses ;
4954let verifyProviderApiKey : typeof import ( "./authDetector" ) . verifyProviderApiKey ;
55+ const originalPlatform = process . platform ;
56+
57+ function setPlatform ( value : NodeJS . Platform ) : void {
58+ Object . defineProperty ( process , "platform" , {
59+ value,
60+ configurable : true ,
61+ } ) ;
62+ }
5063
5164beforeEach ( async ( ) => {
5265 vi . resetModules ( ) ;
66+ setPlatform ( "darwin" ) ;
5367 const mod = await import ( "./authDetector" ) ;
5468 detectAllAuth = mod . detectAllAuth ;
5569 detectCliAuthStatuses = mod . detectCliAuthStatuses ;
@@ -58,17 +72,24 @@ beforeEach(async () => {
5872
5973describe ( "authDetector" , ( ) => {
6074 const originalEnv = { ...process . env } ;
75+ let tempHomeDir : string | null = null ;
6176
6277 beforeEach ( ( ) => {
6378 spawnMock . mockReset ( ) ;
79+ execFileSyncMock . mockReset ( ) ;
6480 getAllApiKeysMock . mockReset ( ) ;
6581 vi . unstubAllGlobals ( ) ;
6682 process . env = { ...originalEnv } ;
6783 } ) ;
6884
6985 afterEach ( ( ) => {
7086 process . env = { ...originalEnv } ;
87+ setPlatform ( originalPlatform ) ;
7188 vi . unstubAllGlobals ( ) ;
89+ if ( tempHomeDir ) {
90+ fs . rmSync ( tempHomeDir , { recursive : true , force : true } ) ;
91+ tempHomeDir = null ;
92+ }
7293 } ) ;
7394
7495 it ( "reports installed-but-unauthenticated CLI providers" , async ( ) => {
@@ -236,6 +257,89 @@ describe("authDetector", () => {
236257 expect ( claude ?. authenticated ) . toBe ( true ) ;
237258 } ) ;
238259
260+ it ( "finds codex through an npm-global prefix when PATH lookup fails" , async ( ) => {
261+ tempHomeDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "ade-auth-detector-" ) ) ;
262+ const prefixDir = path . join ( tempHomeDir , ".npm-global" ) ;
263+ fs . mkdirSync ( path . join ( prefixDir , "bin" ) , { recursive : true } ) ;
264+ fs . writeFileSync ( path . join ( tempHomeDir , ".npmrc" ) , "prefix=~/.npm-global\n" , "utf8" ) ;
265+ fs . writeFileSync ( path . join ( prefixDir , "bin" , "codex" ) , "#!/bin/sh\nexit 0\n" , "utf8" ) ;
266+ fs . chmodSync ( path . join ( prefixDir , "bin" , "codex" ) , 0o755 ) ;
267+ process . env . HOME = tempHomeDir ;
268+ process . env . PATH = "/usr/bin:/bin" ;
269+
270+ spawnMock . mockImplementation ( ( command : string , args : string [ ] = [ ] ) => {
271+ if ( args [ 0 ] === "--version" ) {
272+ if ( command === "codex" ) return fakeError ( ) ;
273+ if ( command === path . join ( prefixDir , "bin" , "codex" ) ) return fakeChild ( { status : 0 , stdout : "0.105.0\n" } ) ;
274+ return fakeError ( ) ;
275+ }
276+ if ( command === "which" ) {
277+ return fakeChild ( { status : 1 } ) ;
278+ }
279+ if ( ( command === "codex" || command . endsWith ( "/codex" ) ) && args [ 0 ] === "login" && args [ 1 ] === "status" ) {
280+ return fakeChild ( { status : 0 , stdout : "Authenticated as test-user\n" } ) ;
281+ }
282+ return fakeChild ( { status : 1 } ) ;
283+ } ) ;
284+
285+ const statuses = await detectCliAuthStatuses ( ) ;
286+ const codex = statuses . find ( ( entry ) => entry . cli === "codex" ) ;
287+
288+ expect ( codex ) . toEqual ( {
289+ cli : "codex" ,
290+ installed : true ,
291+ path : path . join ( prefixDir , "bin" , "codex" ) ,
292+ authenticated : true ,
293+ verified : true ,
294+ } ) ;
295+ } ) ;
296+
297+ it ( "repairs PATH from the interactive shell during a forced refresh" , async ( ) => {
298+ process . env . PATH = "/usr/bin:/bin:/usr/sbin:/sbin" ;
299+ process . env . SHELL = "/bin/zsh" ;
300+
301+ execFileSyncMock . mockImplementation ( ( _command : string , args : string [ ] ) => {
302+ if ( args [ 0 ] === "-lc" ) {
303+ return "__ADE_PATH_START__/Users/arul/.local/bin:/usr/local/bin:/usr/bin:/bin__ADE_PATH_END__" ;
304+ }
305+ if ( args [ 0 ] === "-ic" ) {
306+ return "shell noise\n__ADE_PATH_START__/Users/arul/.npm-global/bin:/Users/arul/.local/bin:/usr/local/bin:/usr/bin:/bin__ADE_PATH_END__" ;
307+ }
308+ throw new Error ( `unexpected shell args: ${ args . join ( " " ) } ` ) ;
309+ } ) ;
310+
311+ spawnMock . mockImplementation ( ( command : string , args : string [ ] = [ ] ) => {
312+ if ( args [ 0 ] === "--version" ) {
313+ if ( command === "codex" && process . env . PATH ?. includes ( "/Users/arul/.npm-global/bin" ) ) {
314+ return fakeChild ( { status : 0 , stdout : "codex-cli 0.117.0\n" } ) ;
315+ }
316+ return fakeError ( ) ;
317+ }
318+ if ( command === "which" ) {
319+ if ( args [ 0 ] === "codex" && process . env . PATH ?. includes ( "/Users/arul/.npm-global/bin" ) ) {
320+ return fakeChild ( { status : 0 , stdout : "/Users/arul/.npm-global/bin/codex\n" } ) ;
321+ }
322+ return fakeChild ( { status : 1 } ) ;
323+ }
324+ if ( ( command === "codex" || command . endsWith ( "/codex" ) ) && args [ 0 ] === "login" && args [ 1 ] === "status" ) {
325+ return fakeChild ( { status : 0 , stdout : "Logged in using ChatGPT\n" } ) ;
326+ }
327+ return fakeChild ( { status : 1 } ) ;
328+ } ) ;
329+
330+ const statuses = await detectCliAuthStatuses ( { force : true } ) ;
331+ const codex = statuses . find ( ( entry ) => entry . cli === "codex" ) ;
332+
333+ expect ( process . env . PATH ) . toContain ( "/Users/arul/.npm-global/bin" ) ;
334+ expect ( codex ) . toEqual ( {
335+ cli : "codex" ,
336+ installed : true ,
337+ path : "/Users/arul/.npm-global/bin/codex" ,
338+ authenticated : true ,
339+ verified : true ,
340+ } ) ;
341+ } ) ;
342+
239343 it ( "verifies API keys with provider endpoints" , async ( ) => {
240344 vi . stubGlobal (
241345 "fetch" ,
0 commit comments