From a76265a7c0edcae7e1b952cacd32e0eca20b6325 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Mon, 13 Apr 2026 15:05:22 +0900 Subject: [PATCH 1/2] fix(image-analysis): wrap defaultDeps in arrow functions to fix circular dep capture Direct function references in defaultDeps (checkRemoteProxy, fetchRemoteAuthStatus, getAuthStatus, getProxyTarget, initializeAccounts) are captured at module load time. Due to circular dependencies in the module graph, auth-handler.js may not have finished initializing when this module is first evaluated, causing these references to be captured as undefined. Wrapping each reference in an arrow function defers evaluation to call time, by which point all modules are fully initialized. isCliproxyRunning was already wrapped correctly; this patch applies the same pattern consistently to all deps. Fixes #973 --- src/utils/hooks/image-analysis-runtime-status.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/hooks/image-analysis-runtime-status.ts b/src/utils/hooks/image-analysis-runtime-status.ts index 4322b9e5..5a4d938e 100644 --- a/src/utils/hooks/image-analysis-runtime-status.ts +++ b/src/utils/hooks/image-analysis-runtime-status.ts @@ -29,11 +29,11 @@ interface ImageAnalysisRuntimeStatusDeps { } const defaultDeps: ImageAnalysisRuntimeStatusDeps = { - checkRemoteProxy, - fetchRemoteAuthStatus, - getAuthStatus, - getProxyTarget, - initializeAccounts, + checkRemoteProxy: (...args) => checkRemoteProxy(...args), + fetchRemoteAuthStatus: (...args) => fetchRemoteAuthStatus(...args), + getAuthStatus: (...args) => getAuthStatus(...args), + getProxyTarget: () => getProxyTarget(), + initializeAccounts: () => initializeAccounts(), isCliproxyRunning: () => isCliproxyRunning(), }; From d74b514dc5b2d741071b13d85ac02da115ae9b9d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 13 Apr 2026 08:45:42 -0400 Subject: [PATCH 2/2] test(image-analysis): cover circular dependency regression --- ...runtime-status-circular-dependency.test.ts | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/unit/utils/hooks/image-analysis-runtime-status-circular-dependency.test.ts diff --git a/tests/unit/utils/hooks/image-analysis-runtime-status-circular-dependency.test.ts b/tests/unit/utils/hooks/image-analysis-runtime-status-circular-dependency.test.ts new file mode 100644 index 00000000..d0b669fd --- /dev/null +++ b/tests/unit/utils/hooks/image-analysis-runtime-status-circular-dependency.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'bun:test'; +import { readFileSync } from 'fs'; +import { dirname } from 'path'; +import type { ImageAnalysisStatus } from '../../../../src/utils/hooks/image-analysis-backend-resolver'; +import type { AuthStatus } from '../../../../src/cliproxy/auth-handler'; +import { + ModuleKind, + ScriptTarget, + transpileModule, + type CompilerOptions, +} from 'typescript'; + +function createStatus(overrides: Partial = {}): ImageAnalysisStatus { + return { + enabled: true, + supported: true, + status: 'active', + backendId: 'ghcp', + backendDisplayName: 'GitHub Copilot (OAuth)', + model: 'claude-haiku-4.5', + resolutionSource: 'profile-backend', + reason: null, + shouldPersistHook: true, + persistencePath: '/tmp/orq.settings.json', + runtimePath: '/api/provider/ghcp', + usesCurrentTarget: true, + usesCurrentAuthToken: true, + hookInstalled: true, + sharedHookInstalled: true, + authReadiness: 'unknown', + authProvider: 'ghcp', + authDisplayName: 'GitHub Copilot (OAuth)', + authReason: 'Auth readiness has not been verified yet.', + proxyReadiness: 'unknown', + proxyReason: 'CLIProxy runtime readiness has not been verified yet.', + effectiveRuntimeMode: 'native-read', + effectiveRuntimeReason: null, + profileModel: 'claude-haiku-4.5', + nativeReadPreference: false, + nativeImageCapable: true, + nativeImageReason: 'claude-haiku-4.5 can read images natively.', + ...overrides, + }; +} + +describe('image-analysis-runtime-status circular dependency regression', () => { + it('reads auth deps from the CommonJS module object at call time', async () => { + const authHandlerModule: { + initializeAccounts: undefined | (() => void); + getAuthStatus: undefined | ((provider: 'ghcp') => AuthStatus); + } = { + initializeAccounts: undefined, + getAuthStatus: undefined, + }; + + const sourcePath = new URL( + '../../../../src/utils/hooks/image-analysis-runtime-status.ts', + import.meta.url + ); + const source = readFileSync(sourcePath, 'utf8'); + const compilerOptions: CompilerOptions = { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES2020, + esModuleInterop: true, + }; + const transpiled = transpileModule(source, { compilerOptions }).outputText; + + const module = { exports: {} as Record }; + const requireMap: Record = { + '../../cliproxy/auth-handler': authHandlerModule, + '../../cliproxy/remote-proxy-client': { + checkRemoteProxy: async () => ({ reachable: false, error: 'not-used' }), + }, + '../../cliproxy/remote-auth-fetcher': { + fetchRemoteAuthStatus: async () => [], + }, + '../../cliproxy/proxy-target-resolver': { + getProxyTarget: () => ({ + host: '127.0.0.1', + port: 8317, + protocol: 'http', + isRemote: false, + }), + }, + '../../cliproxy/provider-capabilities': { + getProviderDisplayName: () => 'GitHub Copilot (OAuth)', + isCLIProxyProvider: () => true, + }, + '../../cliproxy/stats-fetcher': { + isCliproxyRunning: async () => true, + }, + '../../config/unified-config-types': { + DEFAULT_IMAGE_ANALYSIS_CONFIG: {}, + }, + './image-analysis-backend-resolver': { + resolveImageAnalysisStatus: () => createStatus(), + }, + }; + + const compiledModule = new Function( + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + transpiled + ); + compiledModule( + module.exports, + (specifier: string) => { + const dependency = requireMap[specifier]; + if (!dependency) { + throw new Error(`Unexpected dependency: ${specifier}`); + } + return dependency; + }, + module, + sourcePath.pathname, + dirname(sourcePath.pathname) + ); + + authHandlerModule.initializeAccounts = () => {}; + authHandlerModule.getAuthStatus = () => ({ + provider: 'ghcp', + authenticated: true, + tokenDir: '/tmp/auth', + tokenFiles: ['github-copilot-test.json'], + accounts: [], + defaultAccount: undefined, + }); + + const { hydrateImageAnalysisRuntimeStatus } = module.exports as { + hydrateImageAnalysisRuntimeStatus: ( + baseStatus: ImageAnalysisStatus, + deps?: Record + ) => Promise; + }; + const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {}); + + expect(status.authReadiness).toBe('ready'); + expect(status.proxyReadiness).toBe('ready'); + expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis'); + }); +});