test(cliproxy): replace process-wide mock.module with DI seams

Bun's `mock.module()` is process-wide and is NOT undone by `mock.restore()`.
Two test files used module-level mocks that leaked across test runs,
silently breaking unrelated suites that imported the mocked modules
transitively:

- tests/unit/cliproxy/version-checker-stale-cache.test.ts mocked
  binary/version-checker, contaminating cliproxy-stats-routes-install
  and cliproxy-stats-routes-model-update suites that import a route
  module which transitively imports version-checker.
- tests/unit/cliproxy/service-manager-startup.test.ts mocked 8 modules
  at top level (binary-manager, stats-fetcher, etc.), contaminating any
  later file that imports them.

CI happens to be green because file ordering on the GitHub runner places
victim files BEFORE contaminators in `bun test`. On Linux locally the
order is reversed, producing 6 reproducible test failures on every
`bun run test:fast` run. A change in OS, bun version, or new test files
that import the mocked modules could silently flip the CI runner's
order and surface these failures unexpectedly.

Replaced module-level mocks with explicit dependency-injection seams in
production code, following the existing pattern in version-checker.ts
and version-cache.ts (`fetchJsonFn?`, `fetchLatestVersionFn?`):

- src/cliproxy/types.ts: `BinaryManagerConfig.checkForUpdatesFn` (optional)
- src/cliproxy/binary/lifecycle.ts: route through new fn with default
- src/cliproxy/service-manager.ts: `ensureCliproxyService(deps?)` with
  optional `ensureBinaryFn`, `detectRunningProxyFn`,
  `configNeedsRegenerationFn`, `withStartupLockFn`

All deps fields are optional with real-implementation defaults, so
production callers are unchanged. Tests now inject deterministic stubs
through call-site parameters instead of module-level mocks.

Verified locally: \`bun run validate\` is now 2540 pass / 0 fail
(previously 2534 pass / 6 fail at the same upstream/main HEAD).

Built [OnSteroids](https://onsteroids.ai)
This commit is contained in:
Sergey Galuza committed 2026-04-26 09:43:09 +02:00
1 parent 8ccf193fd4
commit a7871283d4
5 files changed
+73 -85

No files matched your search

@@ -1,65 +1,26 @@
import { describe, expect, it, mock } from 'bun:test';
import { describe, expect, it } from 'bun:test';
const ensureBinaryCalls: Array<unknown> = [];
mock.module('../../../src/cliproxy/binary-manager', () => ({
ensureCLIProxyBinary: async (_verbose = false, options?: unknown) => {
ensureBinaryCalls.push(options);
throw new Error(
'CLIProxy Plus binary is not installed locally. Run "ccs cliproxy install" when you have network access.'
);
},
}));
mock.module('../../../src/cliproxy/config-generator', () => ({
ensureConfigDir: () => undefined,
generateConfig: () => '/tmp/cliproxy-config.yaml',
regenerateConfig: () => '/tmp/cliproxy-config.yaml',
configNeedsRegeneration: () => false,
CLIPROXY_DEFAULT_PORT: 8317,
getCliproxyWritablePath: () => '/tmp',
}));
mock.module('../../../src/cliproxy/proxy-detector', () => ({
detectRunningProxy: async () => ({ running: false, verified: false }),
waitForProxyHealthy: async () => false,
}));
mock.module('../../../src/cliproxy/startup-lock', () => ({
withStartupLock: async <T>(fn: () => Promise<T>) => await fn(),
}));
mock.module('../../../src/cliproxy/session-tracker', () => ({
registerSession: () => undefined,
}));
mock.module('../../../src/cliproxy/stats-fetcher', () => ({
isCliproxyRunning: async () => false,
}));
mock.module('../../../src/cliproxy/auth/token-refresh-config', () => ({
getTokenRefreshConfig: () => null,
}));
mock.module('../../../src/cliproxy/auth/token-refresh-worker', () => ({
TokenRefreshWorker: class {
isActive(): boolean {
return false;
}
start(): void {}
stop(): void {}
},
}));
const { ensureCliproxyService } = await import(
`../../../src/cliproxy/service-manager?service-manager-startup=${Date.now()}`
);
import { ensureCliproxyService } from '../../../src/cliproxy/service-manager';
describe('ensureCliproxyService', () => {
it('fails fast without attempting a runtime install when the local binary is missing', async () => {
ensureBinaryCalls.length = 0;
// Inject stubs via the deps seam instead of bun's `mock.module()`. The
// module-level mock approach is process-wide and was previously leaking
// stubbed binary-manager / stats-fetcher modules into unrelated test
// suites (notably cliproxy-stats-routes-*) that import them transitively.
const ensureBinaryCalls: Array<unknown> = [];
const result = await ensureCliproxyService(8317, false);
const result = await ensureCliproxyService(8317, false, {
ensureBinaryFn: async (_verbose, options) => {
ensureBinaryCalls.push(options);
throw new Error(
'CLIProxy Plus binary is not installed locally. Run "ccs cliproxy install" when you have network access.'
);
},
detectRunningProxyFn: async () => ({ running: false, verified: false }),
configNeedsRegenerationFn: () => false,
withStartupLockFn: async (fn) => await fn(),
});
expect(result).toEqual({
started: false,
@@ -91,33 +91,27 @@ describe('version-checker stale cache fallback', () => {
it('skips update lookups when runtime startup prefers the installed binary', async () => {
const { getExecutableName } = await import('../../../src/cliproxy/platform-detector');
const { ensureBinary } = await import('../../../src/cliproxy/binary/lifecycle');
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
fs.writeFileSync(path.join(plusBinDir, getExecutableName('plus')), 'binary');
// Verify the contract via dependency injection rather than mock.module().
// bun's mock.module() is process-wide and is NOT undone by mock.restore(),
// which previously leaked a stubbed version-checker into unrelated test
// files (cliproxy-stats-routes-*) that transitively import it.
let checkForUpdatesCalls = 0;
mock.module('../../../src/cliproxy/binary/version-checker', () => ({
checkForUpdates: async () => {
checkForUpdatesCalls += 1;
return {
hasUpdate: false,
currentVersion: '6.8.2-0',
latestVersion: '6.8.2-0',
fromCache: false,
checkedAt: Date.now(),
};
},
fetchLatestVersion: async () => {
throw new Error('fetchLatestVersion should not run when skipAutoUpdate is enabled');
},
isNewerVersion: () => false,
isVersionFaulty: () => false,
}));
const { ensureBinary } = await import(
`../../../src/cliproxy/binary/lifecycle?skip-auto-update=${Date.now()}`
);
const checkForUpdatesSpy = async () => {
checkForUpdatesCalls += 1;
return {
hasUpdate: false,
currentVersion: '6.8.2-0',
latestVersion: '6.8.2-0',
fromCache: false,
checkedAt: Date.now(),
};
};
const binaryPath = await ensureBinary({
version: '6.8.2-0',
@@ -129,6 +123,7 @@ describe('version-checker stale cache fallback', () => {
skipAutoUpdate: true,
allowInstall: true,
backend: 'plus',
checkForUpdatesFn: checkForUpdatesSpy,
});
expect(binaryPath).toBe(path.join(plusBinDir, getExecutableName('plus')));