mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 22:15:33 +00:00
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:
3 files changed
+76
-90
No files matched your search
@@ -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(() => {
|
||||
|
||||
Reference in new issue
Block a user