Merge pull request #1495 from kaitranntt/kai/fix/bar-launch-ipv4-bind

fix(bar): real-deployment fixes from normal-user install + CI test isolation
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-09 23:11:48 -04:00
commit b966ba369a
3 files changed
+76 -90

No files matched your search

+4 -1
View File
@@ -72,7 +72,10 @@ async function defaultEnsureDashboard(): Promise<DashboardInfo> {
const { startServer } = await import('../../web-server');
const port = await getPort({ port: [3000, 3001, 3002, 8000, 8080] });
const { server } = await startServer({ port });
// Bind IPv4 loopback explicitly. Without a host, startServer defaults to
// 'localhost', which on macOS resolves to ::1 (IPv6) — but bar.json's baseUrl
// (and the Swift app) use 127.0.0.1, so the app could not reach its own server.
const { server } = await startServer({ port, host: '127.0.0.1' });
const addr = server.address();
const resolvedPort = addr && typeof addr === 'object' ? addr.port : port;
@@ -167,17 +167,19 @@ function collectRolloutFiles(
}
/**
* Default tail using Bun.spawn (macOS-safe: no tac, no GNU timeout).
* Reads the last `lines` lines so we never load a huge session into memory.
* Default tail via a pure fs read. Must work under BOTH node and bun: the `ccs`
* CLI (and `ccs bar launch`) runs under node, where `Bun.spawn` is undefined —
* the previous Bun-only implementation threw there, so Codex quota silently
* never surfaced in real deployments (only under the bun-run dev harness).
* Reading the JSONL and slicing the tail is cheap (session rollouts are small)
* and carries no runtime/OS/subprocess dependency.
*/
async function defaultTailLines(file: string, lines: number): Promise<string[]> {
const proc = Bun.spawn(['tail', `-${lines}`, file], {
stdout: 'pipe',
stderr: 'ignore',
});
const text = await new Response(proc.stdout).text();
await proc.exited;
return text.split('\n').filter((l) => l.trim().length > 0);
const text = await fs.promises.readFile(file, 'utf8');
return text
.split('\n')
.filter((l) => l.trim().length > 0)
.slice(-lines);
}
function computeQuotaPercentage(rate: CodexRateLimits): number {
@@ -21,13 +21,15 @@
* a dedicated temp dir set to CCS_HOME before each test. This gives full
* control without touching the facade mock surface.
*
* Note on isolation: Bun runs each test FILE in its own worker process, so
* process.env.CCS_HOME is NOT shared across test files. The previous 2-test
* failure ("clearing tier_lock (null)" and "no tier_lock") was caused by
* within-file state leakage: an earlier test wrote {agy:'ultra',claude:'pro'}
* to disk, and invalidateConfigCache() only cleared the facade's memoisation
* cache — loadOrCreateUnifiedConfig still read the stale disk state. The fix
* is to explicitly re-write the config file in every test that needs null-lock.
* Note on isolation: Bun workers are REUSED across test files within a single
* bun test invocation (Bun 1.3.x, up to --max-concurrency files per worker).
* mock.module() is GLOBAL and STICKY for the lifetime of a worker — it does NOT
* reset between files, and mock.restore() does NOT unwind mock.module() calls.
* To avoid leaking broken stubs to later files (e.g. cliproxy-auth-routes.test.ts,
* which imports validateNickname/hasAccountNameConflict/PROVIDERS_WITHOUT_EMAIL from
* account-manager), every mock factory SPREADS the REAL account-manager and overrides
* ONLY the account-DATA reads + IO that these tier_lock tests control; afterAll then
* restores the real account-manager/account-safety modules outright.
*/
import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
@@ -35,6 +37,17 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { invalidateConfigCache } from '../../../src/config/config-loader-facade';
// Capture the REAL account modules BEFORE the mock.module calls below replace them
// in Bun's global, sticky module registry (mock.module is process-wide and is NOT
// unwound by mock.restore in Bun 1.3.x). We spread these into every mock factory and
// restore them in afterAll, so leak-sensitive helpers that OTHER test files import
// from account-manager (validateNickname, hasAccountNameConflict, registry-path
// getters, PROVIDERS_WITHOUT_EMAIL) stay REAL for any file sharing this Bun worker.
// Only the account-DATA reads + IO are overridden for these tier_lock tests.
import * as realAccountManagerNS from '../../../src/cliproxy/accounts/account-manager';
import * as realAccountSafetyNS from '../../../src/cliproxy/accounts/account-safety';
const REAL_ACCOUNT_MANAGER = { ...realAccountManagerNS };
const REAL_ACCOUNT_SAFETY = { ...realAccountSafetyNS };
// ============================================================================
// Account fixtures
@@ -59,41 +72,39 @@ let _mockPausedIds: Set<string> = new Set();
// Top-level mock.module registrations (file-load time)
// ============================================================================
mock.module('../../../src/cliproxy/accounts/account-manager', () => ({
PROVIDERS_WITHOUT_EMAIL: [],
getAccountsRegistryPath: () => '',
getPausedDir: () => '',
getAccountTokenPath: () => '',
extractAccountIdFromTokenFile: () => '',
deriveNoEmailProviderAccountId: () => '',
generateNickname: () => '',
validateNickname: () => true,
hasAccountNameConflict: () => false,
findAccountNameMatch: () => null,
tokenFileExists: () => false,
loadAccountsRegistry: () => ({}),
saveAccountsRegistry: () => undefined,
syncRegistryWithTokenFiles: () => undefined,
registerAccount: () => undefined,
setDefaultAccount: () => undefined,
pauseAccount: () => undefined,
resumeAccount: () => undefined,
removeAccount: () => undefined,
renameAccount: () => undefined,
touchAccount: () => undefined,
setAccountTier: () => undefined,
discoverExistingAccounts: () => [],
getProviderAccounts: () => _mockAccounts,
getDefaultAccount: () => (_mockAccounts.length > 0 ? _mockAccounts[0] : null),
getAccount: (_p: string, id: string) => _mockAccounts.find((a) => a.id === id) ?? null,
findAccountByQuery: () => null,
getActiveAccounts: () => _mockAccounts,
isAccountPaused: (_p: string, id: string) => _mockPausedIds.has(id),
getAllAccountsSummary: () => [],
bulkPauseAccounts: () => ({ succeeded: [], failed: [] }),
bulkResumeAccounts: () => ({ succeeded: [], failed: [] }),
soloAccount: async () => null,
}));
// Build the account-manager mock: REAL by default (spread) so leak-sensitive helpers
// other files import from account-manager — validateNickname, hasAccountNameConflict,
// registry-path getters, PROVIDERS_WITHOUT_EMAIL — never leak broken stubs to a file
// sharing this Bun worker. Override ONLY the account-DATA reads (test fixtures) and IO
// (no-ops, so the tier_lock tests never touch disk).
function buildAccountManagerMock() {
return {
...REAL_ACCOUNT_MANAGER,
loadAccountsRegistry: () => ({}),
saveAccountsRegistry: () => undefined,
syncRegistryWithTokenFiles: () => undefined,
registerAccount: () => undefined,
setDefaultAccount: () => undefined,
pauseAccount: () => undefined,
resumeAccount: () => undefined,
removeAccount: () => undefined,
renameAccount: () => undefined,
touchAccount: () => undefined,
setAccountTier: () => undefined,
discoverExistingAccounts: () => [],
getProviderAccounts: () => _mockAccounts,
getDefaultAccount: () => (_mockAccounts.length > 0 ? _mockAccounts[0] : null),
getAccount: (_p: string, id: string) => _mockAccounts.find((a) => a.id === id) ?? null,
getActiveAccounts: () => _mockAccounts,
isAccountPaused: (_p: string, id: string) => _mockPausedIds.has(id),
getAllAccountsSummary: () => [],
bulkPauseAccounts: () => ({ succeeded: [], failed: [] }),
bulkResumeAccounts: () => ({ succeeded: [], failed: [] }),
soloAccount: async () => null,
};
}
mock.module('../../../src/cliproxy/accounts/account-manager', () => buildAccountManagerMock());
mock.module('../../../src/cliproxy/accounts/account-safety', () => ({
restoreExpiredQuotaPauses: () => undefined,
@@ -181,43 +192,9 @@ describe('quota-manager findHealthyAccount — tier_lock', () => {
_mockAccounts = [ULTRA_ACCOUNT, PRO_ACCOUNT, PRO_ACCOUNT_2];
_mockPausedIds = new Set();
// Re-register mocks in beforeEach to survive any mock.restore() calls
// from other test suites running in this Bun process.
mock.module('../../../src/cliproxy/accounts/account-manager', () => ({
PROVIDERS_WITHOUT_EMAIL: [],
getAccountsRegistryPath: () => '',
getPausedDir: () => '',
getAccountTokenPath: () => '',
extractAccountIdFromTokenFile: () => '',
deriveNoEmailProviderAccountId: () => '',
generateNickname: () => '',
validateNickname: () => true,
hasAccountNameConflict: () => false,
findAccountNameMatch: () => null,
tokenFileExists: () => false,
loadAccountsRegistry: () => ({}),
saveAccountsRegistry: () => undefined,
syncRegistryWithTokenFiles: () => undefined,
registerAccount: () => undefined,
setDefaultAccount: () => undefined,
pauseAccount: () => undefined,
resumeAccount: () => undefined,
removeAccount: () => undefined,
renameAccount: () => undefined,
touchAccount: () => undefined,
setAccountTier: () => undefined,
discoverExistingAccounts: () => [],
getProviderAccounts: () => _mockAccounts,
getDefaultAccount: () => (_mockAccounts.length > 0 ? _mockAccounts[0] : null),
getAccount: (_p: string, id: string) => _mockAccounts.find((a) => a.id === id) ?? null,
findAccountByQuery: () => null,
getActiveAccounts: () => _mockAccounts,
isAccountPaused: (_p: string, id: string) => _mockPausedIds.has(id),
getAllAccountsSummary: () => [],
bulkPauseAccounts: () => ({ succeeded: [], failed: [] }),
bulkResumeAccounts: () => ({ succeeded: [], failed: [] }),
soloAccount: async () => null,
}));
// Re-register mocks in beforeEach to ensure mutable closures (_mockAccounts,
// _mockPausedIds) are captured fresh for each test.
mock.module('../../../src/cliproxy/accounts/account-manager', () => buildAccountManagerMock());
mock.module('../../../src/cliproxy/accounts/account-safety', () => ({
restoreExpiredQuotaPauses: () => undefined,
@@ -226,7 +203,11 @@ describe('quota-manager findHealthyAccount — tier_lock', () => {
});
afterAll(() => {
mock.restore();
// mock.restore() does NOT unwind mock.module() in Bun 1.3.x. Restore the REAL
// modules (captured at load time) so any file loading after this suite in the
// same Bun worker gets the genuine account-manager/account-safety, not stubs.
mock.module('../../../src/cliproxy/accounts/account-manager', () => REAL_ACCOUNT_MANAGER);
mock.module('../../../src/cliproxy/accounts/account-safety', () => REAL_ACCOUNT_SAFETY);
});
afterEach(() => {