fix(test): run bucket paths explicitly

This commit is contained in:
Tam Nhu Tran
2026-06-14 11:55:01 -04:00
parent 7e6e12d008
commit 27c8a9570b
12 changed files with 668 additions and 304 deletions
+183 -17
View File
@@ -44,8 +44,15 @@ const slowTests = [
// CommonJS-heavy JS suites stay slow by default because many of them mutate // CommonJS-heavy JS suites stay slow by default because many of them mutate
// module cache or process state. Opt them into `test:fast` only after they are // module cache or process state. Opt them into `test:fast` only after they are
// proven stable in the mixed fast bucket. // proven stable in the mixed fast bucket.
const fastJsTests = new Set([ const fastJsTests = new Set(['tests/unit/flag-parsing-simple.test.js']);
'tests/unit/flag-parsing-simple.test.js',
const isolatedTests = new Set([
'tests/unit/targets/codex-adapter-exec.test.ts',
'tests/unit/targets/codex-adapter.test.ts',
'tests/unit/targets/droid-adapter.test.ts',
'tests/unit/targets/target-registry.test.ts',
'tests/unit/utils/fetch-proxy-setup.test.ts',
'tests/unit/web-server/usage/account-attribution.test.ts',
]); ]);
const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/;
@@ -89,6 +96,11 @@ function shouldForceSlow(file) {
return readsBuiltDist(file); return readsBuiltDist(file);
} }
function usesBunTestRunner(relativePath) {
const source = fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
return source.includes('bun:test') || /(^|[^\w.])(?:describe|it)\s*\(/m.test(source);
}
function getSlowSet() { function getSlowSet() {
const discovered = getDiscoveredTests(); const discovered = getDiscoveredTests();
const forceSlow = discovered.filter((file) => shouldForceSlow(file)); const forceSlow = discovered.filter((file) => shouldForceSlow(file));
@@ -99,9 +111,101 @@ function selectBucket(name) {
const discovered = getDiscoveredTests(); const discovered = getDiscoveredTests();
const slowSet = getSlowSet(); const slowSet = getSlowSet();
return name === 'slow' return name === 'slow' ? [...slowSet].sort() : discovered.filter((file) => !slowSet.has(file));
? [...slowSet].sort() }
: discovered.filter((file) => !slowSet.has(file));
function toBunTestPath(relativePath) {
if (
relativePath.startsWith('./') ||
relativePath.startsWith('../') ||
path.isAbsolute(relativePath)
) {
return relativePath;
}
return `./${relativePath}`;
}
function getBunArgs(name, selected = selectBucket(name)) {
const testPaths = selected.map(toBunTestPath);
// Slow bucket forces sequential execution because it spawns subprocesses,
// binds ports, and touches shared state — parallelism causes flakes.
// Fast bucket keeps bun's default parallelism for speed.
return name === 'slow' ? ['test', '--max-concurrency=1', ...testPaths] : ['test', ...testPaths];
}
function shouldRunIsolated(file) {
return file.startsWith('src/') || isolatedTests.has(file) || !usesBunTestRunner(file);
}
function getBunRuns(name, selected = selectBucket(name)) {
const shared = selected.filter((file) => !shouldRunIsolated(file));
const isolated = selected.filter(shouldRunIsolated);
const runs = [];
if (shared.length > 0) {
runs.push({
label: 'shared',
selected: shared,
bunArgs: getBunArgs(name, shared),
quietOnPass: false,
});
}
for (const file of isolated) {
runs.push({
label: file,
selected: [file],
bunArgs: getBunArgs(name, [file]),
quietOnPass: true,
});
}
return runs;
}
function stripAnsi(value) {
return value.replace(/\x1b\[[0-9;]*m/g, '');
}
function parseBunFileCount(output) {
const match = stripAnsi(output).match(/Ran\s+\d+\s+tests?\s+across\s+(\d+)\s+files?/i);
return match ? Number(match[1]) : null;
}
function verifyReportedFileCount(selectedCount, output) {
const reportedCount = parseBunFileCount(output);
if (reportedCount === null) {
return {
ok: false,
message: '[X] Could not find Bun test file count in output.',
reportedCount,
selectedCount,
};
}
if (reportedCount !== selectedCount) {
return {
ok: false,
message:
`[X] Bun ran ${reportedCount} files, but the bucket selected ${selectedCount} files. ` +
'Check test path arguments for filter/path ambiguity.',
reportedCount,
selectedCount,
};
}
return {
ok: true,
reportedCount,
selectedCount,
};
}
function shouldVerifyRunFileCount(run) {
return run.selected.every((file) => usesBunTestRunner(file));
} }
function ensureBuildForSlowBucket() { function ensureBuildForSlowBucket() {
@@ -118,6 +222,54 @@ function ensureBuildForSlowBucket() {
return build.status ?? 1; return build.status ?? 1;
} }
function runBunTest(run) {
const result = spawnSync('bun', run.bunArgs, {
cwd: rootDir,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
shell: process.platform === 'win32',
});
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
const writeOutput = () => {
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
};
if (result.error) {
writeOutput();
console.error(`[X] Failed to run bun test: ${result.error.message}`);
return 1;
}
const exitCode = result.status ?? 1;
if (exitCode !== 0) {
writeOutput();
return exitCode;
}
if (shouldVerifyRunFileCount(run)) {
const countCheck = verifyReportedFileCount(run.selected.length, output);
if (!countCheck.ok) {
writeOutput();
console.error(countCheck.message);
return 1;
}
}
if (run.quietOnPass) {
console.log(`[OK] ${run.label}`);
} else {
writeOutput();
}
return 0;
}
function runBucket(name) { function runBucket(name) {
const selected = selectBucket(name); const selected = selectBucket(name);
@@ -133,20 +285,25 @@ function runBucket(name) {
} }
} }
// Slow bucket forces sequential execution because it spawns subprocesses, const runs = getBunRuns(name, selected);
// binds ports, and touches shared state — parallelism causes flakes. const isolatedCount = runs.filter((run) => run.quietOnPass).length;
// Fast bucket keeps bun's default parallelism for speed. if (isolatedCount > 0) {
const bunArgs = name === 'slow' console.log(`[i] Running ${isolatedCount} test file(s) in isolated Bun processes.`);
? ['test', '--max-concurrency=1', ...selected] }
: ['test', ...selected];
const result = spawnSync('bun', bunArgs, { let exitCode = 0;
cwd: rootDir, for (const run of runs) {
stdio: 'inherit', const status = runBunTest(run);
shell: process.platform === 'win32', if (status !== 0) {
}); exitCode = status;
}
}
return result.status ?? 1; if (exitCode === 0) {
console.log(`[OK] Bucket '${name}' ran ${selected.length} selected test files.`);
}
return exitCode;
} }
function main(args = process.argv.slice(2)) { function main(args = process.argv.slice(2)) {
@@ -180,10 +337,19 @@ if (require.main === module) {
module.exports = { module.exports = {
slowTests, slowTests,
fastJsTests, fastJsTests,
isolatedTests,
readsBuiltDist, readsBuiltDist,
shouldForceSlow, shouldForceSlow,
getDiscoveredTests, getDiscoveredTests,
getSlowSet, getSlowSet,
selectBucket, selectBucket,
toBunTestPath,
getBunArgs,
usesBunTestRunner,
shouldRunIsolated,
getBunRuns,
parseBunFileCount,
verifyReportedFileCount,
shouldVerifyRunFileCount,
main, main,
}; };
@@ -27,7 +27,7 @@ function makeBrowserConfig(enabled = false, policy: 'auto' | 'always' | 'never'
describe('resolveBrowserLaunchFlags — no browser flags', () => { describe('resolveBrowserLaunchFlags — no browser flags', () => {
it('returns undefined override and passes args through unchanged', async () => { it('returns undefined override and passes args through unchanged', async () => {
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (a: string[]) => a, appendBrowserToolArgs: (a: string[]) => a,
resolveBrowserLaunchFlagResolution: (_args: string[]) => ({ resolveBrowserLaunchFlagResolution: (_args: string[]) => ({
override: undefined, override: undefined,
@@ -40,7 +40,7 @@ describe('resolveBrowserLaunchFlags — no browser flags', () => {
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }), resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
syncBrowserMcpToConfigDir: () => true, syncBrowserMcpToConfigDir: () => true,
})); }));
mock.module('../../../config/unified-config-loader', () => ({ mock.module('../../../config/config-loader-facade', () => ({
getBrowserConfig: () => makeBrowserConfig(false), getBrowserConfig: () => makeBrowserConfig(false),
loadOrCreateUnifiedConfig: () => ({}), loadOrCreateUnifiedConfig: () => ({}),
getThinkingConfig: () => ({}), getThinkingConfig: () => ({}),
@@ -58,7 +58,7 @@ describe('resolveBrowserLaunchFlags — no browser flags', () => {
describe('resolveBrowserLaunchFlags — with browser-launch override', () => { describe('resolveBrowserLaunchFlags — with browser-launch override', () => {
it('returns override and strips the browser flag from args', async () => { it('returns override and strips the browser flag from args', async () => {
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (a: string[]) => a, appendBrowserToolArgs: (a: string[]) => a,
resolveBrowserLaunchFlagResolution: (_args: string[]) => ({ resolveBrowserLaunchFlagResolution: (_args: string[]) => ({
override: 'force-enable' as const, override: 'force-enable' as const,
@@ -71,7 +71,7 @@ describe('resolveBrowserLaunchFlags — with browser-launch override', () => {
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }), resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
syncBrowserMcpToConfigDir: () => true, syncBrowserMcpToConfigDir: () => true,
})); }));
mock.module('../../../config/unified-config-loader', () => ({ mock.module('../../../config/config-loader-facade', () => ({
getBrowserConfig: () => makeBrowserConfig(true, 'auto'), getBrowserConfig: () => makeBrowserConfig(true, 'auto'),
loadOrCreateUnifiedConfig: () => ({}), loadOrCreateUnifiedConfig: () => ({}),
getThinkingConfig: () => ({}), getThinkingConfig: () => ({}),
@@ -98,7 +98,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
}); });
it('emits warn() when getBlockedBrowserOverrideWarning returns a message', async () => { it('emits warn() when getBlockedBrowserOverrideWarning returns a message', async () => {
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (a: string[]) => a, appendBrowserToolArgs: (a: string[]) => a,
resolveBrowserLaunchFlagResolution: (args: string[]) => ({ resolveBrowserLaunchFlagResolution: (args: string[]) => ({
override: 'force-enable' as const, override: 'force-enable' as const,
@@ -111,7 +111,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }), resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
syncBrowserMcpToConfigDir: () => true, syncBrowserMcpToConfigDir: () => true,
})); }));
mock.module('../../../config/unified-config-loader', () => ({ mock.module('../../../config/config-loader-facade', () => ({
getBrowserConfig: () => makeBrowserConfig(false, 'never'), getBrowserConfig: () => makeBrowserConfig(false, 'never'),
loadOrCreateUnifiedConfig: () => ({}), loadOrCreateUnifiedConfig: () => ({}),
getThinkingConfig: () => ({}), getThinkingConfig: () => ({}),
@@ -128,7 +128,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
describe('resolveBrowserRuntime — attach disabled', () => { describe('resolveBrowserRuntime — attach disabled', () => {
it('returns undefined browserRuntimeEnv when browser attach is disabled', async () => { it('returns undefined browserRuntimeEnv when browser attach is disabled', async () => {
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (a: string[]) => a, appendBrowserToolArgs: (a: string[]) => a,
resolveBrowserLaunchFlagResolution: (a: string[]) => ({ resolveBrowserLaunchFlagResolution: (a: string[]) => ({
override: undefined, override: undefined,
@@ -141,7 +141,7 @@ describe('resolveBrowserRuntime — attach disabled', () => {
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }), resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
syncBrowserMcpToConfigDir: () => true, syncBrowserMcpToConfigDir: () => true,
})); }));
mock.module('../../../config/unified-config-loader', () => ({ mock.module('../../../config/config-loader-facade', () => ({
getBrowserConfig: () => makeBrowserConfig(false), getBrowserConfig: () => makeBrowserConfig(false),
loadOrCreateUnifiedConfig: () => ({}), loadOrCreateUnifiedConfig: () => ({}),
getThinkingConfig: () => ({}), getThinkingConfig: () => ({}),
@@ -158,7 +158,7 @@ describe('resolveBrowserRuntime — attach disabled', () => {
describe('resolveBrowserRuntime — active runtime env', () => { describe('resolveBrowserRuntime — active runtime env', () => {
it('returns runtimeEnv when browser attach resolves successfully', async () => { it('returns runtimeEnv when browser attach resolves successfully', async () => {
const fakeRuntimeEnv = { CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1:9222/json' }; const fakeRuntimeEnv = { CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1:9222/json' };
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (a: string[]) => a, appendBrowserToolArgs: (a: string[]) => a,
resolveBrowserLaunchFlagResolution: (a: string[]) => ({ resolveBrowserLaunchFlagResolution: (a: string[]) => ({
override: 'force-enable' as const, override: 'force-enable' as const,
@@ -171,7 +171,7 @@ describe('resolveBrowserRuntime — active runtime env', () => {
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: fakeRuntimeEnv }), resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: fakeRuntimeEnv }),
syncBrowserMcpToConfigDir: () => true, syncBrowserMcpToConfigDir: () => true,
})); }));
mock.module('../../../config/unified-config-loader', () => ({ mock.module('../../../config/config-loader-facade', () => ({
getBrowserConfig: () => makeBrowserConfig(true, 'always'), getBrowserConfig: () => makeBrowserConfig(true, 'always'),
loadOrCreateUnifiedConfig: () => ({}), loadOrCreateUnifiedConfig: () => ({}),
getThinkingConfig: () => ({}), getThinkingConfig: () => ({}),
@@ -34,7 +34,7 @@ mock.module('child_process', () => ({
const mockEscapeShellArg = jest.fn((s: string) => `"${s}"`); const mockEscapeShellArg = jest.fn((s: string) => `"${s}"`);
const mockGetWindowsEscapedCommandShell = jest.fn().mockReturnValue('cmd.exe'); const mockGetWindowsEscapedCommandShell = jest.fn().mockReturnValue('cmd.exe');
mock.module('../../utils/shell-executor', () => ({ mock.module('../../../utils/shell-executor', () => ({
escapeShellArg: mockEscapeShellArg, escapeShellArg: mockEscapeShellArg,
getWindowsEscapedCommandShell: mockGetWindowsEscapedCommandShell, getWindowsEscapedCommandShell: mockGetWindowsEscapedCommandShell,
})); }));
@@ -46,7 +46,7 @@ mock.module('../../config/config-generator', () => ({
getProviderConfig: jest.fn(), getProviderConfig: jest.fn(),
})); }));
mock.module('../../utils/websearch-manager', () => ({ mock.module('../../../utils/websearch-manager', () => ({
appendThirdPartyWebSearchToolArgs: (args: string[]) => args, appendThirdPartyWebSearchToolArgs: (args: string[]) => args,
createWebSearchTraceContext: jest.fn().mockReturnValue({}), createWebSearchTraceContext: jest.fn().mockReturnValue({}),
ensureWebSearchMcpOrThrow: jest.fn(), ensureWebSearchMcpOrThrow: jest.fn(),
@@ -54,17 +54,17 @@ mock.module('../../utils/websearch-manager', () => ({
getWebSearchHookEnv: jest.fn().mockReturnValue({}), getWebSearchHookEnv: jest.fn().mockReturnValue({}),
})); }));
mock.module('../../utils/image-analysis', () => ({ mock.module('../../../utils/image-analysis', () => ({
appendThirdPartyImageAnalysisToolArgs: (args: string[]) => [...args, '--mcp-image-analysis'], appendThirdPartyImageAnalysisToolArgs: (args: string[]) => [...args, '--mcp-image-analysis'],
syncImageAnalysisMcpToConfigDir: jest.fn(), syncImageAnalysisMcpToConfigDir: jest.fn(),
ensureImageAnalysisMcpOrThrow: jest.fn().mockReturnValue(true), ensureImageAnalysisMcpOrThrow: jest.fn().mockReturnValue(true),
})); }));
mock.module('../../utils/browser', () => ({ mock.module('../../../utils/browser', () => ({
appendBrowserToolArgs: (args: string[]) => [...args, '--browser'], appendBrowserToolArgs: (args: string[]) => [...args, '--browser'],
})); }));
mock.module('../accounts/account-manager', () => ({ mock.module('../../accounts/account-manager', () => ({
getDefaultAccount: jest.fn().mockReturnValue(null), getDefaultAccount: jest.fn().mockReturnValue(null),
})); }));
@@ -82,7 +82,7 @@ mock.module('../account-resolution', () => ({
})); }));
// Dynamic import for quota-manager // Dynamic import for quota-manager
mock.module('../quota/quota-manager', () => ({ mock.module('../../quota/quota-manager', () => ({
startQuotaMonitor: jest.fn(), startQuotaMonitor: jest.fn(),
stopQuotaMonitor: jest.fn(), stopQuotaMonitor: jest.fn(),
})); }));
@@ -202,7 +202,7 @@ describe('launchClaude', () => {
it('uses shell mode for .cmd executables on Windows', async () => { it('uses shell mode for .cmd executables on Windows', async () => {
await launchClaude(baseContext({ claudeCli: 'C:\\tools\\claude.cmd' })); await launchClaude(baseContext({ claudeCli: 'C:\\tools\\claude.cmd' }));
const spawnOpts = mockSpawn.mock.calls[0][2] as { shell: string | boolean }; const spawnOpts = mockSpawn.mock.calls[0][1] as { shell: string | boolean };
// shell property should be set (cmd.exe from mock) // shell property should be set (cmd.exe from mock)
expect(spawnOpts.shell).toBeTruthy(); expect(spawnOpts.shell).toBeTruthy();
}); });
@@ -5,7 +5,7 @@
* silent for healthy ones, covering both simple and composite providers. * silent for healthy ones, covering both simple and composite providers.
*/ */
import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, jest, mock } from 'bun:test';
// ── Stubs ───────────────────────────────────────────────────────────────────── // ── Stubs ─────────────────────────────────────────────────────────────────────
@@ -23,26 +23,21 @@ const mockGetSuggestedReplacementModel = jest
.fn<string | undefined, [string, string]>() .fn<string | undefined, [string, string]>()
.mockReturnValue(undefined); .mockReturnValue(undefined);
jest.mock('../model-warnings', () => { mock.module('../../config/model-config', () => ({
// We test the real implementation — only mock its dependencies
return jest.requireActual('../model-warnings');
});
jest.mock('../../config/model-config', () => ({
getCurrentModel: mockGetCurrentModel, getCurrentModel: mockGetCurrentModel,
})); }));
jest.mock('../../cliproxy/model-catalog', () => ({ mock.module('../../model-catalog', () => ({
isModelBroken: mockIsModelBroken, isModelBroken: mockIsModelBroken,
getModelIssueUrl: mockGetModelIssueUrl, getModelIssueUrl: mockGetModelIssueUrl,
findModel: mockFindModel, findModel: mockFindModel,
getSuggestedReplacementModel: mockGetSuggestedReplacementModel, getSuggestedReplacementModel: mockGetSuggestedReplacementModel,
})); }));
// We import from real path after jest.mock so actual module is used
import { warnBrokenModels } from '../model-warnings';
import type { ExecutorConfig } from '../../types'; import type { ExecutorConfig } from '../../types';
const { warnBrokenModels } = await import('../model-warnings');
// ── Helpers ──────────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────────
function makeSimpleCfg(overrides: Partial<ExecutorConfig> = {}): ExecutorConfig { function makeSimpleCfg(overrides: Partial<ExecutorConfig> = {}): ExecutorConfig {
@@ -150,6 +150,7 @@ export async function resolveAccounts(
} }
} }
process.exit(1); process.exit(1);
return { earlyExit: true };
} }
setDefaultAccount(provider, account.id); setDefaultAccount(provider, account.id);
touchAccount(provider, account.id); touchAccount(provider, account.id);
@@ -166,6 +167,7 @@ export async function resolveAccounts(
console.error(fail(`No account found for ${providerConfig.displayName}`)); console.error(fail(`No account found for ${providerConfig.displayName}`));
console.error(` Run "ccs ${provider} --auth" to add an account first`); console.error(` Run "ccs ${provider} --auth" to add an account first`);
process.exit(1); process.exit(1);
return { earlyExit: true };
} }
try { try {
const success = renameAccount(provider, defaultAccount.id, setNickname); const success = renameAccount(provider, defaultAccount.id, setNickname);
@@ -174,12 +176,15 @@ export async function resolveAccounts(
} else { } else {
console.error(fail('Failed to rename account')); console.error(fail('Failed to rename account'));
process.exit(1); process.exit(1);
return { earlyExit: true };
} }
} catch (err) { } catch (err) {
console.error(fail(err instanceof Error ? err.message : 'Failed to rename account')); console.error(fail(err instanceof Error ? err.message : 'Failed to rename account'));
process.exit(1); process.exit(1);
return { earlyExit: true };
} }
process.exit(0); process.exit(0);
return { earlyExit: true };
} }
return { earlyExit: false }; return { earlyExit: false };
@@ -12,12 +12,14 @@
* by a direct atomic write; the running-proxy PATCH path is covered at the * by a direct atomic write; the running-proxy PATCH path is covered at the
* clearDrainOrderPriorities unit level (drain-order.test.ts). * clearDrainOrderPriorities unit level (drain-order.test.ts).
*/ */
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { handleOrderSubcommand } from '../order-subcommand'; mock.module('../../../cliproxy/proxy/proxy-detector', () => ({
detectRunningProxy: async () => ({ running: false, verified: false }),
}));
describe('handleOrderSubcommand', () => { describe('handleOrderSubcommand', () => {
let tempHome: string; let tempHome: string;
@@ -52,6 +54,13 @@ describe('handleOrderSubcommand', () => {
return import(`../../../cliproxy/accounts/registry?order-subcommand=${Date.now()}`); return import(`../../../cliproxy/accounts/registry?order-subcommand=${Date.now()}`);
} }
async function runOrderSubcommand(args: string[]): Promise<void> {
const { handleOrderSubcommand } = await import(
`../order-subcommand?order-subcommand=${Date.now()}`
);
await handleOrderSubcommand(args);
}
beforeEach(() => { beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-order-subcommand-')); tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-order-subcommand-'));
originalCcsHome = process.env.CCS_HOME; originalCcsHome = process.env.CCS_HOME;
@@ -86,7 +95,7 @@ describe('handleOrderSubcommand', () => {
registerAccount('claude', 'claude-a.json', 'a@x.com'); registerAccount('claude', 'claude-a.json', 'a@x.com');
registerAccount('claude', 'claude-b.json', 'b@x.com'); registerAccount('claude', 'claude-b.json', 'b@x.com');
await handleOrderSubcommand(['claude']); await runOrderSubcommand(['claude']);
const output = lines.join('\n'); const output = lines.join('\n');
// b@x.com (priority 5) must appear before a@x.com (priority 1). // b@x.com (priority 5) must appear before a@x.com (priority 1).
@@ -113,7 +122,7 @@ describe('handleOrderSubcommand', () => {
registerAccount('claude', 'claude-a.json', 'a@x.com'); registerAccount('claude', 'claude-a.json', 'a@x.com');
registerAccount('claude', 'claude-b.json', 'b@x.com'); registerAccount('claude', 'claude-b.json', 'b@x.com');
await handleOrderSubcommand(['claude']); await runOrderSubcommand(['claude']);
const output = lines.join('\n'); const output = lines.join('\n');
expect(output).toContain('no priority set'); expect(output).toContain('no priority set');
@@ -131,7 +140,7 @@ describe('handleOrderSubcommand', () => {
registerAccount('claude', 'claude-b.json', 'b@x.com'); registerAccount('claude', 'claude-b.json', 'b@x.com');
saveDrainOrderConfig('claude', { mode: 'manual', orderedIds: ['a@x.com', 'b@x.com'] }); saveDrainOrderConfig('claude', { mode: 'manual', orderedIds: ['a@x.com', 'b@x.com'] });
await handleOrderSubcommand(['claude', '--reset']); await runOrderSubcommand(['claude', '--reset']);
// Residual priority is actually gone from disk (not just config deleted). // Residual priority is actually gone from disk (not just config deleted).
expect('priority' in readAuthFile('claude-a.json')).toBe(false); expect('priority' in readAuthFile('claude-a.json')).toBe(false);
@@ -149,7 +158,7 @@ describe('handleOrderSubcommand', () => {
const { registerAccount } = await registerClaude(); const { registerAccount } = await registerClaude();
registerAccount('claude', 'claude-a.json', 'a@x.com'); registerAccount('claude', 'claude-a.json', 'a@x.com');
await handleOrderSubcommand(['claude', '--reset']); await runOrderSubcommand(['claude', '--reset']);
const output = lines.join('\n'); const output = lines.join('\n');
expect(output).toContain('reset to file order'); expect(output).toContain('reset to file order');
+1
View File
@@ -231,6 +231,7 @@ export class DeltaAccumulator {
if (usage) { if (usage) {
this.inputTokens = usage.prompt_tokens || usage.input_tokens || 0; this.inputTokens = usage.prompt_tokens || usage.input_tokens || 0;
this.outputTokens = usage.completion_tokens || usage.output_tokens || 0; this.outputTokens = usage.completion_tokens || usage.output_tokens || 0;
this.usageReceived = true;
} }
} }
+253 -184
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
const GlmtTransformer = require('../dist/glmt/glmt-transformer').default; const GlmtTransformer = require('../../dist/glmt/glmt-transformer').default;
const { DeltaAccumulator } = require('../dist/glmt/delta-accumulator'); const { DeltaAccumulator } = require('../../dist/glmt/delta-accumulator');
/** /**
* Token Counting Validation Tests * Token Counting Validation Tests
@@ -60,8 +60,8 @@ function assertEqual(actual, expected, message) {
if (actual !== expected) { if (actual !== expected) {
throw new Error( throw new Error(
`${message || 'Assertion failed'}\n` + `${message || 'Assertion failed'}\n` +
` Expected: ${JSON.stringify(expected)}\n` + ` Expected: ${JSON.stringify(expected)}\n` +
` Actual: ${JSON.stringify(actual)}` ` Actual: ${JSON.stringify(actual)}`
); );
} }
} }
@@ -84,8 +84,8 @@ function assertDeepEqual(actual, expected, message) {
if (actualStr !== expectedStr) { if (actualStr !== expectedStr) {
throw new Error( throw new Error(
`${message || 'Deep equality failed'}\n` + `${message || 'Deep equality failed'}\n` +
` Expected: ${expectedStr}\n` + ` Expected: ${expectedStr}\n` +
` Actual: ${actualStr}` ` Actual: ${actualStr}`
); );
} }
} }
@@ -102,14 +102,14 @@ runner.test('message_delta includes both input_tokens and output_tokens', () =>
// Simulate usage data // Simulate usage data
accumulator.updateUsage({ accumulator.updateUsage({
prompt_tokens: 150, prompt_tokens: 150,
completion_tokens: 75 completion_tokens: 75,
}); });
accumulator.finishReason = 'stop'; accumulator.finishReason = 'stop';
const events = transformer.finalizeDelta(accumulator); const events = transformer.finalizeDelta(accumulator);
// Find message_delta event // Find message_delta event
const messageDelta = events.find(e => e.event === 'message_delta'); const messageDelta = events.find((e) => e.event === 'message_delta');
assertExists(messageDelta, 'message_delta event should exist'); assertExists(messageDelta, 'message_delta event should exist');
assertExists(messageDelta.data.usage, 'usage should exist in message_delta'); assertExists(messageDelta.data.usage, 'usage should exist in message_delta');
assertEqual(messageDelta.data.usage.input_tokens, 150, 'input_tokens should be 150'); assertEqual(messageDelta.data.usage.input_tokens, 150, 'input_tokens should be 150');
@@ -124,18 +124,20 @@ runner.test('token counting with simple prompt (no tools)', () => {
const openaiResponse = { const openaiResponse = {
id: 'chatcmpl-123', id: 'chatcmpl-123',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: 'Simple response' role: 'assistant',
content: 'Simple response',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}],
usage: { usage: {
prompt_tokens: 10, prompt_tokens: 10,
completion_tokens: 5, completion_tokens: 5,
total_tokens: 15 total_tokens: 15,
} },
}; };
const result = transformer.transformResponse(openaiResponse, {}); const result = transformer.transformResponse(openaiResponse, {});
@@ -153,26 +155,30 @@ runner.test('token counting with tool calls', () => {
const openaiResponse = { const openaiResponse = {
id: 'chatcmpl-456', id: 'chatcmpl-456',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: null, role: 'assistant',
tool_calls: [{ content: null,
id: 'call_1', tool_calls: [
type: 'function', {
function: { id: 'call_1',
name: 'get_weather', type: 'function',
arguments: '{"location":"London"}' function: {
} name: 'get_weather',
}] arguments: '{"location":"London"}',
},
},
],
},
finish_reason: 'tool_calls',
}, },
finish_reason: 'tool_calls' ],
}],
usage: { usage: {
prompt_tokens: 50, prompt_tokens: 50,
completion_tokens: 25, completion_tokens: 25,
total_tokens: 75 total_tokens: 75,
} },
}; };
const result = transformer.transformResponse(openaiResponse, {}); const result = transformer.transformResponse(openaiResponse, {});
@@ -181,7 +187,10 @@ runner.test('token counting with tool calls', () => {
assertEqual(result.usage.input_tokens, 50, 'input_tokens should be 50'); assertEqual(result.usage.input_tokens, 50, 'input_tokens should be 50');
assertEqual(result.usage.output_tokens, 25, 'output_tokens should be 25'); assertEqual(result.usage.output_tokens, 25, 'output_tokens should be 25');
assertEqual(result.stop_reason, 'tool_use', 'stop_reason should be tool_use'); assertEqual(result.stop_reason, 'tool_use', 'stop_reason should be tool_use');
assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block'); assertTrue(
result.content.some((b) => b.type === 'tool_use'),
'should have tool_use block'
);
}); });
// ======================================== // ========================================
@@ -192,27 +201,31 @@ runner.test('token counting with thinking blocks and tool calls', () => {
const openaiResponse = { const openaiResponse = {
id: 'chatcmpl-789', id: 'chatcmpl-789',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
reasoning_content: 'Let me analyze this request...', role: 'assistant',
content: 'I need to call a tool', reasoning_content: 'Let me analyze this request...',
tool_calls: [{ content: 'I need to call a tool',
id: 'call_2', tool_calls: [
type: 'function', {
function: { id: 'call_2',
name: 'calculate', type: 'function',
arguments: '{"expression":"2+2"}' function: {
} name: 'calculate',
}] arguments: '{"expression":"2+2"}',
},
},
],
},
finish_reason: 'tool_calls',
}, },
finish_reason: 'tool_calls' ],
}],
usage: { usage: {
prompt_tokens: 100, prompt_tokens: 100,
completion_tokens: 80, completion_tokens: 80,
total_tokens: 180 total_tokens: 180,
} },
}; };
const result = transformer.transformResponse(openaiResponse, {}); const result = transformer.transformResponse(openaiResponse, {});
@@ -220,8 +233,14 @@ runner.test('token counting with thinking blocks and tool calls', () => {
assertExists(result.usage, 'usage should exist'); assertExists(result.usage, 'usage should exist');
assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100'); assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100');
assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80'); assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80');
assertTrue(result.content.some(b => b.type === 'thinking'), 'should have thinking block'); assertTrue(
assertTrue(result.content.some(b => b.type === 'tool_use'), 'should have tool_use block'); result.content.some((b) => b.type === 'thinking'),
'should have thinking block'
);
assertTrue(
result.content.some((b) => b.type === 'tool_use'),
'should have tool_use block'
);
}); });
// ======================================== // ========================================
@@ -242,17 +261,19 @@ runner.test('deferred finalization waits for usage data', () => {
const openaiEvent1 = { const openaiEvent1 = {
event: 'data', event: 'data',
data: { data: {
choices: [{ choices: [
delta: {}, {
finish_reason: 'stop' delta: {},
}] finish_reason: 'stop',
} },
],
},
}; };
const events1 = transformer.transformDelta(openaiEvent1, accumulator); const events1 = transformer.transformDelta(openaiEvent1, accumulator);
// Should NOT have message_stop event yet // Should NOT have message_stop event yet
const hasMessageStop1 = events1.some(e => e.event === 'message_stop'); const hasMessageStop1 = events1.some((e) => e.event === 'message_stop');
assertEqual(hasMessageStop1, false, 'should NOT finalize without usage'); assertEqual(hasMessageStop1, false, 'should NOT finalize without usage');
assertEqual(accumulator.finalized, false, 'accumulator should NOT be finalized'); assertEqual(accumulator.finalized, false, 'accumulator should NOT be finalized');
@@ -262,15 +283,15 @@ runner.test('deferred finalization waits for usage data', () => {
data: { data: {
usage: { usage: {
prompt_tokens: 200, prompt_tokens: 200,
completion_tokens: 100 completion_tokens: 100,
} },
} },
}; };
const events2 = transformer.transformDelta(openaiEvent2, accumulator); const events2 = transformer.transformDelta(openaiEvent2, accumulator);
// Should NOW finalize since we have both finish_reason AND usage // Should NOW finalize since we have both finish_reason AND usage
const hasMessageStop2 = events2.some(e => e.event === 'message_stop'); const hasMessageStop2 = events2.some((e) => e.event === 'message_stop');
assertEqual(hasMessageStop2, true, 'should finalize when usage arrives'); assertEqual(hasMessageStop2, true, 'should finalize when usage arrives');
assertEqual(accumulator.finalized, true, 'accumulator should be finalized'); assertEqual(accumulator.finalized, true, 'accumulator should be finalized');
assertEqual(accumulator.usageReceived, true, 'usageReceived should be true'); assertEqual(accumulator.usageReceived, true, 'usageReceived should be true');
@@ -290,9 +311,9 @@ runner.test('finalization waits for BOTH finish_reason AND usage', () => {
data: { data: {
usage: { usage: {
prompt_tokens: 50, prompt_tokens: 50,
completion_tokens: 30 completion_tokens: 30,
} },
} },
}; };
const events1 = transformer.transformDelta(openaiEvent1, accumulator); const events1 = transformer.transformDelta(openaiEvent1, accumulator);
@@ -303,18 +324,20 @@ runner.test('finalization waits for BOTH finish_reason AND usage', () => {
const openaiEvent2 = { const openaiEvent2 = {
event: 'data', event: 'data',
data: { data: {
choices: [{ choices: [
delta: {}, {
finish_reason: 'stop' delta: {},
}] finish_reason: 'stop',
} },
],
},
}; };
const events2 = transformer.transformDelta(openaiEvent2, accumulator); const events2 = transformer.transformDelta(openaiEvent2, accumulator);
assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set'); assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set');
assertEqual(accumulator.finalized, true, 'should finalize when both present'); assertEqual(accumulator.finalized, true, 'should finalize when both present');
const messageDelta = events2.find(e => e.event === 'message_delta'); const messageDelta = events2.find((e) => e.event === 'message_delta');
assertExists(messageDelta, 'message_delta should exist'); assertExists(messageDelta, 'message_delta should exist');
assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_tokens in message_delta'); assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_tokens in message_delta');
assertEqual(messageDelta.data.usage.output_tokens, 30, 'output_tokens in message_delta'); assertEqual(messageDelta.data.usage.output_tokens, 30, 'output_tokens in message_delta');
@@ -333,14 +356,14 @@ runner.test('graceful degradation when usage never arrives', () => {
// Simulate [DONE] event without usage // Simulate [DONE] event without usage
const doneEvent = { const doneEvent = {
event: 'done' event: 'done',
}; };
const events = transformer.transformDelta(doneEvent, accumulator); const events = transformer.transformDelta(doneEvent, accumulator);
// Should finalize with zero tokens (graceful degradation) // Should finalize with zero tokens (graceful degradation)
assertEqual(accumulator.finalized, true, 'should finalize on [DONE]'); assertEqual(accumulator.finalized, true, 'should finalize on [DONE]');
const messageDelta = events.find(e => e.event === 'message_delta'); const messageDelta = events.find((e) => e.event === 'message_delta');
assertExists(messageDelta, 'message_delta should exist'); assertExists(messageDelta, 'message_delta should exist');
assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_tokens should be 0'); assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_tokens should be 0');
assertEqual(messageDelta.data.usage.output_tokens, 0, 'output_tokens should be 0'); assertEqual(messageDelta.data.usage.output_tokens, 0, 'output_tokens should be 0');
@@ -358,10 +381,12 @@ runner.test('no regression: thinking blocks still work', () => {
event: 'data', event: 'data',
data: { data: {
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
delta: { role: 'assistant' } {
}] delta: { role: 'assistant' },
} },
],
},
}; };
transformer.transformDelta(event1, accumulator); transformer.transformDelta(event1, accumulator);
@@ -369,25 +394,25 @@ runner.test('no regression: thinking blocks still work', () => {
const event2 = { const event2 = {
event: 'data', event: 'data',
data: { data: {
choices: [{ choices: [
delta: { {
reasoning_content: 'Analyzing the problem...' delta: {
} reasoning_content: 'Analyzing the problem...',
}] },
} },
],
},
}; };
const events2 = transformer.transformDelta(event2, accumulator); const events2 = transformer.transformDelta(event2, accumulator);
// Check thinking block was created // Check thinking block was created
const hasThinkingStart = events2.some(e => const hasThinkingStart = events2.some(
e.event === 'content_block_start' && (e) => e.event === 'content_block_start' && e.data.content_block.type === 'thinking'
e.data.content_block.type === 'thinking'
); );
assertEqual(hasThinkingStart, true, 'thinking block should start'); assertEqual(hasThinkingStart, true, 'thinking block should start');
const hasThinkingDelta = events2.some(e => const hasThinkingDelta = events2.some(
e.event === 'content_block_delta' && (e) => e.event === 'content_block_delta' && e.data.delta.type === 'thinking_delta'
e.data.delta.type === 'thinking_delta'
); );
assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted'); assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted');
}); });
@@ -404,34 +429,36 @@ runner.test('no regression: tool calls execute correctly', () => {
const event = { const event = {
event: 'data', event: 'data',
data: { data: {
choices: [{ choices: [
delta: { {
tool_calls: [{ delta: {
index: 0, tool_calls: [
id: 'call_abc', {
type: 'function', index: 0,
function: { id: 'call_abc',
name: 'search', type: 'function',
arguments: '{"q":"test"}' function: {
} name: 'search',
}] arguments: '{"q":"test"}',
} },
}] },
} ],
},
},
],
},
}; };
const events = transformer.transformDelta(event, accumulator); const events = transformer.transformDelta(event, accumulator);
const toolUseStart = events.find(e => const toolUseStart = events.find(
e.event === 'content_block_start' && (e) => e.event === 'content_block_start' && e.data.content_block.type === 'tool_use'
e.data.content_block.type === 'tool_use'
); );
assertExists(toolUseStart, 'tool_use block should start'); assertExists(toolUseStart, 'tool_use block should start');
assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search'); assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search');
const inputJsonDelta = events.find(e => const inputJsonDelta = events.find(
e.event === 'content_block_delta' && (e) => e.event === 'content_block_delta' && e.data.delta.type === 'input_json_delta'
e.data.delta.type === 'input_json_delta'
); );
assertExists(inputJsonDelta, 'input_json_delta should be emitted'); assertExists(inputJsonDelta, 'input_json_delta should be emitted');
}); });
@@ -448,32 +475,41 @@ runner.test('no regression: streaming still works', () => {
event: 'data', event: 'data',
data: { data: {
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ delta: { role: 'assistant' } }] choices: [{ delta: { role: 'assistant' } }],
} },
}; };
const events1 = transformer.transformDelta(event1, accumulator); const events1 = transformer.transformDelta(event1, accumulator);
assertTrue(events1.some(e => e.event === 'message_start'), 'message_start event'); assertTrue(
events1.some((e) => e.event === 'message_start'),
'message_start event'
);
// Text delta // Text delta
const event2 = { const event2 = {
event: 'data', event: 'data',
data: { data: {
choices: [{ delta: { content: 'Hello' } }] choices: [{ delta: { content: 'Hello' } }],
} },
}; };
const events2 = transformer.transformDelta(event2, accumulator); const events2 = transformer.transformDelta(event2, accumulator);
assertTrue(events2.some(e => e.event === 'content_block_start'), 'content_block_start'); assertTrue(
assertTrue(events2.some(e => e.event === 'content_block_delta'), 'content_block_delta'); events2.some((e) => e.event === 'content_block_start'),
'content_block_start'
);
assertTrue(
events2.some((e) => e.event === 'content_block_delta'),
'content_block_delta'
);
// More text // More text
const event3 = { const event3 = {
event: 'data', event: 'data',
data: { data: {
choices: [{ delta: { content: ' world' } }] choices: [{ delta: { content: ' world' } }],
} },
}; };
const events3 = transformer.transformDelta(event3, accumulator); const events3 = transformer.transformDelta(event3, accumulator);
const textDelta = events3.find(e => e.data?.delta?.type === 'text_delta'); const textDelta = events3.find((e) => e.data?.delta?.type === 'text_delta');
assertExists(textDelta, 'text_delta should exist'); assertExists(textDelta, 'text_delta should exist');
assertEqual(textDelta.data.delta.text, ' world', 'delta text should be " world"'); assertEqual(textDelta.data.delta.text, ' world', 'delta text should be " world"');
}); });
@@ -487,27 +523,35 @@ runner.test('no regression: buffered mode (non-streaming) works', () => {
const openaiResponse = { const openaiResponse = {
id: 'chatcmpl-buffered', id: 'chatcmpl-buffered',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
reasoning_content: 'Thinking step by step...', role: 'assistant',
content: 'Final answer' reasoning_content: 'Thinking step by step...',
content: 'Final answer',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}],
usage: { usage: {
prompt_tokens: 20, prompt_tokens: 20,
completion_tokens: 15, completion_tokens: 15,
total_tokens: 35 total_tokens: 35,
} },
}; };
const result = transformer.transformResponse(openaiResponse, {}); const result = transformer.transformResponse(openaiResponse, {});
assertEqual(result.type, 'message', 'type should be message'); assertEqual(result.type, 'message', 'type should be message');
assertEqual(result.role, 'assistant', 'role should be assistant'); assertEqual(result.role, 'assistant', 'role should be assistant');
assertTrue(result.content.some(b => b.type === 'thinking'), 'has thinking'); assertTrue(
assertTrue(result.content.some(b => b.type === 'text'), 'has text'); result.content.some((b) => b.type === 'thinking'),
'has thinking'
);
assertTrue(
result.content.some((b) => b.type === 'text'),
'has text'
);
assertEqual(result.usage.input_tokens, 20, 'input_tokens'); assertEqual(result.usage.input_tokens, 20, 'input_tokens');
assertEqual(result.usage.output_tokens, 15, 'output_tokens'); assertEqual(result.usage.output_tokens, 15, 'output_tokens');
}); });
@@ -522,7 +566,7 @@ runner.test('usageReceived flag is set when usage data arrives', () => {
accumulator.updateUsage({ accumulator.updateUsage({
prompt_tokens: 100, prompt_tokens: 100,
completion_tokens: 50 completion_tokens: 50,
}); });
assertEqual(accumulator.usageReceived, true, 'should be true after updateUsage'); assertEqual(accumulator.usageReceived, true, 'should be true after updateUsage');
@@ -558,65 +602,87 @@ runner.test('streaming: token counts with thinking + text + tools', () => {
const accumulator = new DeltaAccumulator(); const accumulator = new DeltaAccumulator();
// Message start // Message start
transformer.transformDelta({ transformer.transformDelta(
event: 'data', {
data: { event: 'data',
model: 'GLM-4.6', data: {
choices: [{ delta: { role: 'assistant' } }] model: 'GLM-4.6',
} choices: [{ delta: { role: 'assistant' } }],
}, accumulator); },
},
accumulator
);
// Thinking // Thinking
transformer.transformDelta({ transformer.transformDelta(
event: 'data', {
data: { event: 'data',
choices: [{ delta: { reasoning_content: 'Thinking...' } }] data: {
} choices: [{ delta: { reasoning_content: 'Thinking...' } }],
}, accumulator); },
},
accumulator
);
// Text // Text
transformer.transformDelta({ transformer.transformDelta(
event: 'data', {
data: { event: 'data',
choices: [{ delta: { content: 'Answer' } }] data: {
} choices: [{ delta: { content: 'Answer' } }],
}, accumulator); },
},
accumulator
);
// Tool call // Tool call
transformer.transformDelta({ transformer.transformDelta(
event: 'data', {
data: { event: 'data',
choices: [{ data: {
delta: { choices: [
tool_calls: [{ {
index: 0, delta: {
id: 'call_1', tool_calls: [
type: 'function', {
function: { name: 'tool', arguments: '{}' } index: 0,
}] id: 'call_1',
} type: 'function',
}] function: { name: 'tool', arguments: '{}' },
} },
}, accumulator); ],
},
},
],
},
},
accumulator
);
// Usage arrives // Usage arrives
transformer.transformDelta({ transformer.transformDelta(
event: 'data', {
data: { event: 'data',
usage: { prompt_tokens: 300, completion_tokens: 200 } data: {
} usage: { prompt_tokens: 300, completion_tokens: 200 },
}, accumulator); },
},
accumulator
);
// finish_reason arrives // finish_reason arrives
const finalEvents = transformer.transformDelta({ const finalEvents = transformer.transformDelta(
event: 'data', {
data: { event: 'data',
choices: [{ delta: {}, finish_reason: 'tool_calls' }] data: {
} choices: [{ delta: {}, finish_reason: 'tool_calls' }],
}, accumulator); },
},
accumulator
);
// Verify message_delta has correct tokens // Verify message_delta has correct tokens
const messageDelta = finalEvents.find(e => e.event === 'message_delta'); const messageDelta = finalEvents.find((e) => e.event === 'message_delta');
assertExists(messageDelta, 'message_delta should exist'); assertExists(messageDelta, 'message_delta should exist');
assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300'); assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300');
assertEqual(messageDelta.data.usage.output_tokens, 200, 'output_tokens should be 200'); assertEqual(messageDelta.data.usage.output_tokens, 200, 'output_tokens should be 200');
@@ -624,9 +690,12 @@ runner.test('streaming: token counts with thinking + text + tools', () => {
}); });
// Run all tests // Run all tests
runner.run().then(success => { runner
process.exit(success ? 0 : 1); .run()
}).catch(error => { .then((success) => {
console.error('Test runner error:', error); process.exit(success ? 0 : 1);
process.exit(1); })
}); .catch((error) => {
console.error('Test runner error:', error);
process.exit(1);
});
+20 -15
View File
@@ -7,10 +7,9 @@ const API_KEY = process.env.Z_AI_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
const MODEL = 'GLM-4.6'; const MODEL = 'GLM-4.6';
if (!API_KEY || API_KEY === 'your-api-key-here') { if (!API_KEY || API_KEY === 'your-api-key-here') {
console.error('[ERROR] Z.AI API key not found'); console.log('[SKIP] Z.AI API key not found');
console.error('[INFO] Set Z_AI_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable'); console.log('[INFO] Set Z_AI_API_KEY or ANTHROPIC_AUTH_TOKEN to run this external smoke test');
console.error('[INFO] Example: export Z_AI_API_KEY=your-key-here'); process.exit(0);
process.exit(1);
} }
// Test request with reasoning // Test request with reasoning
@@ -19,14 +18,15 @@ const requestBody = JSON.stringify({
messages: [ messages: [
{ {
role: 'user', role: 'user',
content: 'Solve this math problem step by step: What is 27 * 453? Show your reasoning process.' content:
} 'Solve this math problem step by step: What is 27 * 453? Show your reasoning process.',
},
], ],
stream: true, stream: true,
reasoning: true, reasoning: true,
reasoning_effort: 'medium', reasoning_effort: 'medium',
max_tokens: 4096, max_tokens: 4096,
do_sample: true do_sample: true,
}); });
const options = { const options = {
@@ -36,10 +36,10 @@ const options = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`, Authorization: `Bearer ${API_KEY}`,
'Content-Length': Buffer.byteLength(requestBody), 'Content-Length': Buffer.byteLength(requestBody),
'User-Agent': 'CCS-GLMT-StreamingTest/1.0' 'User-Agent': 'CCS-GLMT-StreamingTest/1.0',
} },
}; };
console.log('═══════════════════════════════════════════════════════════════'); console.log('═══════════════════════════════════════════════════════════════');
@@ -63,7 +63,7 @@ const req = https.request(options, (res) => {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
let errorBody = ''; let errorBody = '';
res.on('data', chunk => errorBody += chunk); res.on('data', (chunk) => (errorBody += chunk));
res.on('end', () => { res.on('end', () => {
console.error('[ERROR] Request failed:', res.statusCode, res.statusMessage); console.error('[ERROR] Request failed:', res.statusCode, res.statusMessage);
console.error('[ERROR] Response:', errorBody); console.error('[ERROR] Response:', errorBody);
@@ -98,7 +98,7 @@ const req = https.request(options, (res) => {
// Keep incomplete line in buffer // Keep incomplete line in buffer
buffer = lines.pop() || ''; buffer = lines.pop() || '';
lines.forEach(line => { lines.forEach((line) => {
if (line.startsWith('data: ')) { if (line.startsWith('data: ')) {
eventCount++; eventCount++;
const data = line.substring(6); const data = line.substring(6);
@@ -124,7 +124,10 @@ const req = https.request(options, (res) => {
console.log(`[EVENT ${eventCount}] *** REASONING IN DELTA ***`); console.log(`[EVENT ${eventCount}] *** REASONING IN DELTA ***`);
console.log(' Delta keys:', Object.keys(delta).join(', ')); console.log(' Delta keys:', Object.keys(delta).join(', '));
console.log(' Reasoning chunk length:', delta.reasoning_content.length); console.log(' Reasoning chunk length:', delta.reasoning_content.length);
console.log(' Reasoning chunk preview:', JSON.stringify(delta.reasoning_content.substring(0, 80))); console.log(
' Reasoning chunk preview:',
JSON.stringify(delta.reasoning_content.substring(0, 80))
);
console.log(''); console.log('');
} }
@@ -134,7 +137,10 @@ const req = https.request(options, (res) => {
console.log(`[EVENT ${eventCount}] *** REASONING IN MESSAGE (COMPLETE) ***`); console.log(`[EVENT ${eventCount}] *** REASONING IN MESSAGE (COMPLETE) ***`);
console.log(' Message keys:', Object.keys(message).join(', ')); console.log(' Message keys:', Object.keys(message).join(', '));
console.log(' Reasoning total length:', message.reasoning_content.length); console.log(' Reasoning total length:', message.reasoning_content.length);
console.log(' Reasoning preview:', message.reasoning_content.substring(0, 100).replace(/\n/g, ' ')); console.log(
' Reasoning preview:',
message.reasoning_content.substring(0, 100).replace(/\n/g, ' ')
);
console.log(''); console.log('');
} }
@@ -159,7 +165,6 @@ const req = https.request(options, (res) => {
console.log(`[EVENT ${eventCount}] Usage:`, JSON.stringify(parsed.usage)); console.log(`[EVENT ${eventCount}] Usage:`, JSON.stringify(parsed.usage));
console.log(''); console.log('');
} }
} catch (e) { } catch (e) {
console.log(`[EVENT ${eventCount}] [PARSE ERROR]`, e.message); console.log(`[EVENT ${eventCount}] [PARSE ERROR]`, e.message);
console.log(' Raw:', data.substring(0, 100)); console.log(' Raw:', data.substring(0, 100));
+54 -41
View File
@@ -10,7 +10,12 @@ const GlmtTransformer = require('../../../dist/glmt/glmt-transformer').default;
* Manual test for debug mode file logging * Manual test for debug mode file logging
*/ */
const logDir = path.join(os.homedir(), '.ccs', 'logs'); const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-debug-mode-test-'));
const logDir = path.join(testRoot, 'logs');
process.on('exit', () => {
fs.rmSync(testRoot, { recursive: true, force: true });
});
// Clean up any existing logs // Clean up any existing logs
console.log('Cleaning up existing logs...'); console.log('Cleaning up existing logs...');
@@ -20,12 +25,12 @@ if (fs.existsSync(logDir)) {
console.log('\n=== Test 1: Debug Mode OFF (default) ==='); console.log('\n=== Test 1: Debug Mode OFF (default) ===');
{ {
const transformer = new GlmtTransformer({ verbose: false }); const transformer = new GlmtTransformer({ verbose: false, debugLogDir: logDir });
console.log(`Debug logging: ${transformer.debugLog}`); console.log(`Debug logging: ${transformer.debugLog}`);
const input = { const input = {
model: 'claude-sonnet-4.5', model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'Test without debug' }] messages: [{ role: 'user', content: 'Test without debug' }],
}; };
const { openaiRequest } = transformer.transformRequest(input); const { openaiRequest } = transformer.transformRequest(input);
@@ -33,15 +38,17 @@ console.log('\n=== Test 1: Debug Mode OFF (default) ===');
const openaiResponse = { const openaiResponse = {
id: 'test-1', id: 'test-1',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: 'Response without debug', role: 'assistant',
reasoning_content: 'Some reasoning...' content: 'Response without debug',
reasoning_content: 'Some reasoning...',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
}; };
transformer.transformResponse(openaiResponse, {}); transformer.transformResponse(openaiResponse, {});
@@ -59,13 +66,13 @@ console.log('\n=== Test 1: Debug Mode OFF (default) ===');
console.log('\n=== Test 2: Debug Mode ON (via config) ==='); console.log('\n=== Test 2: Debug Mode ON (via config) ===');
{ {
const transformer = new GlmtTransformer({ verbose: true, debugLog: true }); const transformer = new GlmtTransformer({ verbose: true, debugLog: true, debugLogDir: logDir });
console.log(`Debug logging: ${transformer.debugLog}`); console.log(`Debug logging: ${transformer.debugLog}`);
console.log(`Log directory: ${transformer.debugLogDir}`); console.log(`Log directory: ${transformer.debugLogDir}`);
const input = { const input = {
model: 'claude-sonnet-4.5', model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'Test with debug' }] messages: [{ role: 'user', content: 'Test with debug' }],
}; };
const { openaiRequest } = transformer.transformRequest(input); const { openaiRequest } = transformer.transformRequest(input);
@@ -73,15 +80,17 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
const openaiResponse = { const openaiResponse = {
id: 'test-2', id: 'test-2',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: 'Response with debug', role: 'assistant',
reasoning_content: 'Detailed reasoning process for debugging...' content: 'Response with debug',
reasoning_content: 'Detailed reasoning process for debugging...',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
}; };
transformer.transformResponse(openaiResponse, {}); transformer.transformResponse(openaiResponse, {});
@@ -105,10 +114,10 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
} }
// Check file names // Check file names
const hasRequestAnthropic = files.some(f => f.includes('request-anthropic')); const hasRequestAnthropic = files.some((f) => f.includes('request-anthropic'));
const hasRequestOpenai = files.some(f => f.includes('request-openai')); const hasRequestOpenai = files.some((f) => f.includes('request-openai'));
const hasResponseOpenai = files.some(f => f.includes('response-openai')); const hasResponseOpenai = files.some((f) => f.includes('response-openai'));
const hasResponseAnthropic = files.some(f => f.includes('response-anthropic')); const hasResponseAnthropic = files.some((f) => f.includes('response-anthropic'));
console.log(`\nFile types found:`); console.log(`\nFile types found:`);
console.log(` request-anthropic: ${hasRequestAnthropic ? '✓' : '✗'}`); console.log(` request-anthropic: ${hasRequestAnthropic ? '✓' : '✗'}`);
@@ -122,25 +131,27 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
} }
// Check file contents // Check file contents
const responseOpenaiFile = files.find(f => f.includes('response-openai')); const responseOpenaiFile = files.find((f) => f.includes('response-openai'));
const responseOpenaiPath = path.join(logDir, responseOpenaiFile); const responseOpenaiPath = path.join(logDir, responseOpenaiFile);
const responseOpenaiData = JSON.parse(fs.readFileSync(responseOpenaiPath, 'utf8')); const responseOpenaiData = JSON.parse(fs.readFileSync(responseOpenaiPath, 'utf8'));
console.log(`\nChecking response-openai.json for reasoning_content...`); console.log(`\nChecking response-openai.json for reasoning_content...`);
if (responseOpenaiData.choices[0].message.reasoning_content) { if (responseOpenaiData.choices[0].message.reasoning_content) {
console.log('✓ reasoning_content found in response-openai.json'); console.log('✓ reasoning_content found in response-openai.json');
console.log(` Length: ${responseOpenaiData.choices[0].message.reasoning_content.length} chars`); console.log(
` Length: ${responseOpenaiData.choices[0].message.reasoning_content.length} chars`
);
} else { } else {
console.log('ERROR: reasoning_content NOT found in response-openai.json!'); console.log('ERROR: reasoning_content NOT found in response-openai.json!');
process.exit(1); process.exit(1);
} }
const responseAnthropicFile = files.find(f => f.includes('response-anthropic')); const responseAnthropicFile = files.find((f) => f.includes('response-anthropic'));
const responseAnthropicPath = path.join(logDir, responseAnthropicFile); const responseAnthropicPath = path.join(logDir, responseAnthropicFile);
const responseAnthropicData = JSON.parse(fs.readFileSync(responseAnthropicPath, 'utf8')); const responseAnthropicData = JSON.parse(fs.readFileSync(responseAnthropicPath, 'utf8'));
console.log(`\nChecking response-anthropic.json for thinking block...`); console.log(`\nChecking response-anthropic.json for thinking block...`);
const thinkingBlock = responseAnthropicData.content.find(b => b.type === 'thinking'); const thinkingBlock = responseAnthropicData.content.find((b) => b.type === 'thinking');
if (thinkingBlock) { if (thinkingBlock) {
console.log('✓ Thinking block found in response-anthropic.json'); console.log('✓ Thinking block found in response-anthropic.json');
console.log(` Length: ${thinkingBlock.thinking.length} chars`); console.log(` Length: ${thinkingBlock.thinking.length} chars`);
@@ -158,12 +169,12 @@ console.log('\n=== Test 3: Debug Mode via CCS_DEBUG=1 ===');
fs.rmSync(logDir, { recursive: true, force: true }); fs.rmSync(logDir, { recursive: true, force: true });
process.env.CCS_DEBUG = '1'; process.env.CCS_DEBUG = '1';
const transformer = new GlmtTransformer({ verbose: false }); const transformer = new GlmtTransformer({ verbose: false, debugLogDir: logDir });
console.log(`Debug logging: ${transformer.debugLog}`); console.log(`Debug logging: ${transformer.debugLog}`);
const input = { const input = {
model: 'claude-sonnet-4.5', model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'Test with env var' }] messages: [{ role: 'user', content: 'Test with env var' }],
}; };
transformer.transformRequest(input); transformer.transformRequest(input);
@@ -171,15 +182,17 @@ console.log('\n=== Test 3: Debug Mode via CCS_DEBUG=1 ===');
const openaiResponse = { const openaiResponse = {
id: 'test-3', id: 'test-3',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: 'Response', role: 'assistant',
reasoning_content: 'Reasoning...' content: 'Response',
reasoning_content: 'Reasoning...',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
}; };
transformer.transformResponse(openaiResponse, {}); transformer.transformResponse(openaiResponse, {});
@@ -211,14 +224,14 @@ console.log('\n=== Test 4: Error Handling (No Write Permission) ===');
const transformer = new GlmtTransformer({ const transformer = new GlmtTransformer({
debugLog: true, debugLog: true,
debugLogDir: readOnlyDir, debugLogDir: readOnlyDir,
verbose: false verbose: false,
}); });
console.log('Testing graceful error handling with read-only directory...'); console.log('Testing graceful error handling with read-only directory...');
const input = { const input = {
model: 'claude-sonnet-4.5', model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'Test error handling' }] messages: [{ role: 'user', content: 'Test error handling' }],
}; };
try { try {
+18 -14
View File
@@ -2,28 +2,35 @@
'use strict'; 'use strict';
const GlmtTransformer = require('../../../dist/glmt/glmt-transformer').default; const GlmtTransformer = require('../../../dist/glmt/glmt-transformer').default;
const fs = require('fs');
const path = require('path');
const os = require('os');
console.log('=== Performance Test: Debug Mode Impact ===\n'); console.log('=== Performance Test: Debug Mode Impact ===\n');
const iterations = 1000; const iterations = 1000;
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-performance-test-'));
const logDir = path.join(testRoot, 'logs');
const sampleRequest = { const sampleRequest = {
model: 'claude-sonnet-4.5', model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'Test performance' }] messages: [{ role: 'user', content: 'Test performance' }],
}; };
const sampleResponse = { const sampleResponse = {
id: 'perf-test', id: 'perf-test',
model: 'GLM-4.6', model: 'GLM-4.6',
choices: [{ choices: [
message: { {
role: 'assistant', message: {
content: 'Quick response', role: 'assistant',
reasoning_content: 'Some reasoning content here...' content: 'Quick response',
reasoning_content: 'Some reasoning content here...',
},
finish_reason: 'stop',
}, },
finish_reason: 'stop' ],
}], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
}; };
// Test 1: Debug OFF // Test 1: Debug OFF
@@ -41,7 +48,7 @@ console.log(` Average per request: ${(timeOff / iterations).toFixed(2)}ms`);
// Test 2: Debug ON // Test 2: Debug ON
console.log(`\nTest 2: Debug ON (${iterations} iterations)`); console.log(`\nTest 2: Debug ON (${iterations} iterations)`);
const transformerOn = new GlmtTransformer({ debugLog: true, verbose: false }); const transformerOn = new GlmtTransformer({ debugLog: true, verbose: false, debugLogDir: logDir });
const startOn = Date.now(); const startOn = Date.now();
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
transformerOn.transformRequest(sampleRequest); transformerOn.transformRequest(sampleRequest);
@@ -66,11 +73,8 @@ console.log(`\nNote: Debug mode is opt-in only and disabled by default.`);
console.log(`When disabled, there is NO performance impact (early return in _writeDebugLog).`); console.log(`When disabled, there is NO performance impact (early return in _writeDebugLog).`);
// Cleanup // Cleanup
const fs = require('fs');
const path = require('path');
const os = require('os');
const logDir = path.join(os.homedir(), '.ccs', 'logs');
if (fs.existsSync(logDir)) { if (fs.existsSync(logDir)) {
fs.rmSync(logDir, { recursive: true, force: true }); fs.rmSync(logDir, { recursive: true, force: true });
console.log(`\nCleaned up ${iterations * 4} debug log files.`); console.log(`\nCleaned up ${iterations * 4} debug log files.`);
} }
fs.rmSync(testRoot, { recursive: true, force: true });
@@ -44,6 +44,103 @@ describe('run-test-bucket', () => {
expect(discovered).toContain('src/cliproxy/types/__tests__/types-backward-compat.test.ts'); expect(discovered).toContain('src/cliproxy/types/__tests__/types-backward-compat.test.ts');
}); });
test('passes selected tests to Bun as explicit relative paths', () => {
const args = bucket.getBunArgs('fast', [
'tests/unit/example.test.ts',
'src/cliproxy/types/__tests__/types-backward-compat.test.ts',
]);
expect(args).toEqual([
'test',
'./tests/unit/example.test.ts',
'./src/cliproxy/types/__tests__/types-backward-compat.test.ts',
]);
});
test('keeps slow bucket concurrency flag before explicit test paths', () => {
const args = bucket.getBunArgs('slow', ['tests/integration/example.test.ts']);
expect(args).toEqual(['test', '--max-concurrency=1', './tests/integration/example.test.ts']);
});
test('isolates src tests while keeping already-covered tests in a shared run', () => {
const runs = bucket.getBunRuns('fast', [
'tests/unit/scripts/run-test-bucket.test.js',
'src/cliproxy/types/__tests__/types-backward-compat.test.ts',
'src/errors/__tests__/error-types.test.ts',
]);
expect(runs.map((run) => run.label)).toEqual([
'shared',
'src/cliproxy/types/__tests__/types-backward-compat.test.ts',
'src/errors/__tests__/error-types.test.ts',
]);
expect(runs[0].selected).toEqual(['tests/unit/scripts/run-test-bucket.test.js']);
expect(runs[0].bunArgs).toEqual(['test', './tests/unit/scripts/run-test-bucket.test.js']);
expect(runs[1].bunArgs).toEqual([
'test',
'./src/cliproxy/types/__tests__/types-backward-compat.test.ts',
]);
expect(runs[1].quietOnPass).toBe(true);
});
test('isolates known sticky mock suites outside src', () => {
const runs = bucket.getBunRuns('fast', [
'tests/unit/scripts/run-test-bucket.test.js',
'tests/unit/targets/target-registry.test.ts',
]);
expect(runs.map((run) => run.label)).toEqual([
'shared',
'tests/unit/targets/target-registry.test.ts',
]);
});
test('isolates standalone validation scripts that are not Bun test suites', () => {
const runs = bucket.getBunRuns('slow', [
'tests/integration/cursor-daemon-lifecycle.test.ts',
'tests/integration/token-counting-test.js',
]);
expect(bucket.usesBunTestRunner('tests/integration/cursor-daemon-lifecycle.test.ts')).toBe(
true
);
expect(bucket.usesBunTestRunner('tests/integration/token-counting-test.js')).toBe(false);
expect(runs.map((run) => run.label)).toEqual([
'shared',
'tests/integration/token-counting-test.js',
]);
expect(runs[1].quietOnPass).toBe(true);
});
test('parses Bun file counts from test summaries', () => {
expect(bucket.parseBunFileCount('\u001b[32mRan 2559 tests across 272 files\u001b[0m')).toBe(
272
);
});
test('detects when Bun reports fewer files than the bucket selected', () => {
const result = bucket.verifyReportedFileCount(364, 'Ran 2559 tests across 272 files');
expect(result.ok).toBe(false);
expect(result.reportedCount).toBe(272);
expect(result.selectedCount).toBe(364);
expect(result.message).toContain('Bun ran 272 files');
});
test('skips Bun file-count verification for standalone validation scripts', () => {
expect(
bucket.shouldVerifyRunFileCount({
selected: ['tests/integration/token-counting-test.js'],
})
).toBe(false);
expect(
bucket.shouldVerifyRunFileCount({
selected: ['tests/integration/cursor-daemon-lifecycle.test.ts'],
})
).toBe(true);
});
test('keeps dist-independent javascript tests in the fast bucket', () => { test('keeps dist-independent javascript tests in the fast bucket', () => {
expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false); expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false);
}); });