From 2f8745bc1f54f12caa773e716a03e71b6b44bec0 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 20:08:07 -0400 Subject: [PATCH 1/4] fix(bar): bind 'ccs bar launch' server to IPv4 loopback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit startServer was called without a host, defaulting to 'localhost' which resolves to ::1 (IPv6) on macOS, while bar.json's baseUrl and the menu-bar app use 127.0.0.1 — so the app could not reach its own server (connection refused) and showed offline. Pass host 127.0.0.1 explicitly to match. --- src/commands/bar/launch-subcommand.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/bar/launch-subcommand.ts b/src/commands/bar/launch-subcommand.ts index 89cc33ef..8476c753 100644 --- a/src/commands/bar/launch-subcommand.ts +++ b/src/commands/bar/launch-subcommand.ts @@ -72,7 +72,10 @@ async function defaultEnsureDashboard(): Promise { 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; From 2e99b58d095b847d0f8862b634262ae79a5666d5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 21:11:01 -0400 Subject: [PATCH 2/4] fix(bar): read codex rollout tail via fs so quota surfaces under node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defaultTailLines used Bun.spawn(['tail',...]), but the ccs server (and 'ccs bar launch') runs under node where Bun is undefined — it threw, the per-path catch swallowed it, and Codex quota silently never appeared in real deployments (it only worked under the bun-run dev harness). Replace with a pure fs read: runtime- and OS-agnostic, no subprocess. Verified under node: surfaces a live rollout's rate_limits (primary/secondary). --- .../usage/codex-local-quota-collector.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/web-server/usage/codex-local-quota-collector.ts b/src/web-server/usage/codex-local-quota-collector.ts index ffea21b5..82e077a8 100644 --- a/src/web-server/usage/codex-local-quota-collector.ts +++ b/src/web-server/usage/codex-local-quota-collector.ts @@ -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 { - 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 { From 0770b8903748c83fd1debe3c7c5d489ae72b1c3a Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 21:11:10 -0400 Subject: [PATCH 3/4] fix(test): stop tier-lock mock.module leaking an empty PROVIDERS_WITHOUT_EMAIL bun's mock.module is global and sticky; this test mocked account-manager with PROVIDERS_WITHOUT_EMAIL: [] at top level and in beforeEach, poisoning the module registry for later files in the same bun worker. cliproxy-auth-routes imports PROVIDERS_WITHOUT_EMAIL at load time, got [], and getStartAuthNicknameError's guard returned null for all providers -> 9 flaky CI failures (ordering-dependent). Use the real ['kiro','ghcp'] in every mock factory and re-register safe defaults in afterAll. Verified: worst-case ordering 39/39, full fast bucket 2500/0. --- .../cliproxy/quota-manager-tier-lock.test.ts | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/tests/unit/cliproxy/quota-manager-tier-lock.test.ts b/tests/unit/cliproxy/quota-manager-tier-lock.test.ts index 0ba6a66a..3b9e5015 100644 --- a/tests/unit/cliproxy/quota-manager-tier-lock.test.ts +++ b/tests/unit/cliproxy/quota-manager-tier-lock.test.ts @@ -21,13 +21,16 @@ * 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 prevent leaking a wrong PROVIDERS_WITHOUT_EMAIL value to later files (e.g. + * cliproxy-auth-routes.test.ts), we use the REAL value ['kiro','ghcp'] in every + * mock factory below. The tier_lock tests never assert on PROVIDERS_WITHOUT_EMAIL + * directly, so this has no effect on their correctness. + * The afterAll re-registers the mock with the correct value as a belt-and-suspenders + * guard against any beforeEach re-registration that runs after the last test. */ import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; @@ -60,14 +63,16 @@ let _mockPausedIds: Set = new Set(); // ============================================================================ mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ - PROVIDERS_WITHOUT_EMAIL: [], + // Use the real value so this mock does not corrupt PROVIDERS_WITHOUT_EMAIL for + // later test files loaded in the same Bun worker (mock.module is sticky/global). + PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], getAccountsRegistryPath: () => '', getPausedDir: () => '', getAccountTokenPath: () => '', extractAccountIdFromTokenFile: () => '', deriveNoEmailProviderAccountId: () => '', generateNickname: () => '', - validateNickname: () => true, + validateNickname: () => null, hasAccountNameConflict: () => false, findAccountNameMatch: () => null, tokenFileExists: () => false, @@ -181,17 +186,19 @@ 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. + // Re-register mocks in beforeEach to ensure mutable closures (_mockAccounts, + // _mockPausedIds) are captured fresh for each test. mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ - PROVIDERS_WITHOUT_EMAIL: [], + // Real value — must NOT be [] to avoid leaking a broken PROVIDERS_WITHOUT_EMAIL + // to later test files in the same Bun worker process. + PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], getAccountsRegistryPath: () => '', getPausedDir: () => '', getAccountTokenPath: () => '', extractAccountIdFromTokenFile: () => '', deriveNoEmailProviderAccountId: () => '', generateNickname: () => '', - validateNickname: () => true, + validateNickname: () => null, hasAccountNameConflict: () => false, findAccountNameMatch: () => null, tokenFileExists: () => false, @@ -226,7 +233,48 @@ describe('quota-manager findHealthyAccount — tier_lock', () => { }); afterAll(() => { - mock.restore(); + // mock.restore() does NOT unwind mock.module() in Bun 1.3.x. + // Re-register with the real PROVIDERS_WITHOUT_EMAIL so any test file that + // loads after this suite in the same worker gets a correct account-manager. + mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ + PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], + getAccountsRegistryPath: () => '', + getPausedDir: () => '', + getAccountTokenPath: () => '', + extractAccountIdFromTokenFile: () => '', + deriveNoEmailProviderAccountId: () => '', + generateNickname: () => null, + validateNickname: () => null, + 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: () => [], + getDefaultAccount: () => null, + getAccount: () => null, + findAccountByQuery: () => null, + getActiveAccounts: () => [], + isAccountPaused: () => false, + getAllAccountsSummary: () => [], + bulkPauseAccounts: () => ({ succeeded: [], failed: [] }), + bulkResumeAccounts: () => ({ succeeded: [], failed: [] }), + soloAccount: async () => null, + })); + mock.module('../../../src/cliproxy/accounts/account-safety', () => ({ + restoreExpiredQuotaPauses: () => undefined, + pauseAccountForQuotaCooldown: () => false, + })); }); afterEach(() => { From ad9bdd5794963b25dce2e3da49b4a0ad18b7fe75 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 21:25:21 -0400 Subject: [PATCH 4/4] fix(test): restore real account modules in tier-lock to fully stop mock leak The prior fix only corrected PROVIDERS_WITHOUT_EMAIL, but the mock still stubbed validateNickname -> null, hasAccountNameConflict -> false, and registry paths -> '', which leak to cliproxy-auth-routes (same bun worker) and broke nickname validation + registry ops (9 CI failures). Now every mock factory spreads the REAL account-manager and overrides only the account-DATA reads + IO; afterAll restores the real account-manager/account-safety modules. tier-lock 8/0, combined ordering 39/0, tsc clean. --- .../cliproxy/quota-manager-tier-lock.test.ts | 177 ++++++------------ 1 file changed, 55 insertions(+), 122 deletions(-) diff --git a/tests/unit/cliproxy/quota-manager-tier-lock.test.ts b/tests/unit/cliproxy/quota-manager-tier-lock.test.ts index 3b9e5015..b9304af4 100644 --- a/tests/unit/cliproxy/quota-manager-tier-lock.test.ts +++ b/tests/unit/cliproxy/quota-manager-tier-lock.test.ts @@ -25,12 +25,11 @@ * 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 prevent leaking a wrong PROVIDERS_WITHOUT_EMAIL value to later files (e.g. - * cliproxy-auth-routes.test.ts), we use the REAL value ['kiro','ghcp'] in every - * mock factory below. The tier_lock tests never assert on PROVIDERS_WITHOUT_EMAIL - * directly, so this has no effect on their correctness. - * The afterAll re-registers the mock with the correct value as a belt-and-suspenders - * guard against any beforeEach re-registration that runs after the last test. + * 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'; @@ -38,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 @@ -62,43 +72,39 @@ let _mockPausedIds: Set = new Set(); // Top-level mock.module registrations (file-load time) // ============================================================================ -mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ - // Use the real value so this mock does not corrupt PROVIDERS_WITHOUT_EMAIL for - // later test files loaded in the same Bun worker (mock.module is sticky/global). - PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], - getAccountsRegistryPath: () => '', - getPausedDir: () => '', - getAccountTokenPath: () => '', - extractAccountIdFromTokenFile: () => '', - deriveNoEmailProviderAccountId: () => '', - generateNickname: () => '', - validateNickname: () => null, - 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, @@ -188,43 +194,7 @@ describe('quota-manager findHealthyAccount — tier_lock', () => { // Re-register mocks in beforeEach to ensure mutable closures (_mockAccounts, // _mockPausedIds) are captured fresh for each test. - mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ - // Real value — must NOT be [] to avoid leaking a broken PROVIDERS_WITHOUT_EMAIL - // to later test files in the same Bun worker process. - PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], - getAccountsRegistryPath: () => '', - getPausedDir: () => '', - getAccountTokenPath: () => '', - extractAccountIdFromTokenFile: () => '', - deriveNoEmailProviderAccountId: () => '', - generateNickname: () => '', - validateNickname: () => null, - 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, - })); + mock.module('../../../src/cliproxy/accounts/account-manager', () => buildAccountManagerMock()); mock.module('../../../src/cliproxy/accounts/account-safety', () => ({ restoreExpiredQuotaPauses: () => undefined, @@ -233,48 +203,11 @@ describe('quota-manager findHealthyAccount — tier_lock', () => { }); afterAll(() => { - // mock.restore() does NOT unwind mock.module() in Bun 1.3.x. - // Re-register with the real PROVIDERS_WITHOUT_EMAIL so any test file that - // loads after this suite in the same worker gets a correct account-manager. - mock.module('../../../src/cliproxy/accounts/account-manager', () => ({ - PROVIDERS_WITHOUT_EMAIL: ['kiro', 'ghcp'], - getAccountsRegistryPath: () => '', - getPausedDir: () => '', - getAccountTokenPath: () => '', - extractAccountIdFromTokenFile: () => '', - deriveNoEmailProviderAccountId: () => '', - generateNickname: () => null, - validateNickname: () => null, - 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: () => [], - getDefaultAccount: () => null, - getAccount: () => null, - findAccountByQuery: () => null, - getActiveAccounts: () => [], - isAccountPaused: () => false, - getAllAccountsSummary: () => [], - bulkPauseAccounts: () => ({ succeeded: [], failed: [] }), - bulkResumeAccounts: () => ({ succeeded: [], failed: [] }), - soloAccount: async () => null, - })); - mock.module('../../../src/cliproxy/accounts/account-safety', () => ({ - restoreExpiredQuotaPauses: () => undefined, - pauseAccountForQuotaCooldown: () => false, - })); + // 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(() => {