mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
Merge pull request #1520 from kaitranntt/kai/fix/1516-test-bucket-paths
fix(test): run bucket paths explicitly
This commit is contained in:
+183
-17
@@ -44,8 +44,15 @@ const slowTests = [
|
||||
// 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
|
||||
// proven stable in the mixed fast bucket.
|
||||
const fastJsTests = new Set([
|
||||
'tests/unit/flag-parsing-simple.test.js',
|
||||
const fastJsTests = new Set(['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)$/;
|
||||
@@ -89,6 +96,11 @@ function shouldForceSlow(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() {
|
||||
const discovered = getDiscoveredTests();
|
||||
const forceSlow = discovered.filter((file) => shouldForceSlow(file));
|
||||
@@ -99,9 +111,101 @@ function selectBucket(name) {
|
||||
const discovered = getDiscoveredTests();
|
||||
const slowSet = getSlowSet();
|
||||
|
||||
return name === 'slow'
|
||||
? [...slowSet].sort()
|
||||
: discovered.filter((file) => !slowSet.has(file));
|
||||
return name === 'slow' ? [...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() {
|
||||
@@ -118,6 +222,54 @@ function ensureBuildForSlowBucket() {
|
||||
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) {
|
||||
const selected = selectBucket(name);
|
||||
|
||||
@@ -133,20 +285,25 @@ function runBucket(name) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
const bunArgs = name === 'slow'
|
||||
? ['test', '--max-concurrency=1', ...selected]
|
||||
: ['test', ...selected];
|
||||
const runs = getBunRuns(name, selected);
|
||||
const isolatedCount = runs.filter((run) => run.quietOnPass).length;
|
||||
if (isolatedCount > 0) {
|
||||
console.log(`[i] Running ${isolatedCount} test file(s) in isolated Bun processes.`);
|
||||
}
|
||||
|
||||
const result = spawnSync('bun', bunArgs, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
let exitCode = 0;
|
||||
for (const run of runs) {
|
||||
const status = runBunTest(run);
|
||||
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)) {
|
||||
@@ -180,10 +337,19 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
slowTests,
|
||||
fastJsTests,
|
||||
isolatedTests,
|
||||
readsBuiltDist,
|
||||
shouldForceSlow,
|
||||
getDiscoveredTests,
|
||||
getSlowSet,
|
||||
selectBucket,
|
||||
toBunTestPath,
|
||||
getBunArgs,
|
||||
usesBunTestRunner,
|
||||
shouldRunIsolated,
|
||||
getBunRuns,
|
||||
parseBunFileCount,
|
||||
verifyReportedFileCount,
|
||||
shouldVerifyRunFileCount,
|
||||
main,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ function makeBrowserConfig(enabled = false, policy: 'auto' | 'always' | 'never'
|
||||
|
||||
describe('resolveBrowserLaunchFlags — no browser flags', () => {
|
||||
it('returns undefined override and passes args through unchanged', async () => {
|
||||
mock.module('../../utils/browser', () => ({
|
||||
mock.module('../../../utils/browser', () => ({
|
||||
appendBrowserToolArgs: (a: string[]) => a,
|
||||
resolveBrowserLaunchFlagResolution: (_args: string[]) => ({
|
||||
override: undefined,
|
||||
@@ -40,7 +40,7 @@ describe('resolveBrowserLaunchFlags — no browser flags', () => {
|
||||
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
|
||||
syncBrowserMcpToConfigDir: () => true,
|
||||
}));
|
||||
mock.module('../../../config/unified-config-loader', () => ({
|
||||
mock.module('../../../config/config-loader-facade', () => ({
|
||||
getBrowserConfig: () => makeBrowserConfig(false),
|
||||
loadOrCreateUnifiedConfig: () => ({}),
|
||||
getThinkingConfig: () => ({}),
|
||||
@@ -58,7 +58,7 @@ describe('resolveBrowserLaunchFlags — no browser flags', () => {
|
||||
|
||||
describe('resolveBrowserLaunchFlags — with browser-launch override', () => {
|
||||
it('returns override and strips the browser flag from args', async () => {
|
||||
mock.module('../../utils/browser', () => ({
|
||||
mock.module('../../../utils/browser', () => ({
|
||||
appendBrowserToolArgs: (a: string[]) => a,
|
||||
resolveBrowserLaunchFlagResolution: (_args: string[]) => ({
|
||||
override: 'force-enable' as const,
|
||||
@@ -71,7 +71,7 @@ describe('resolveBrowserLaunchFlags — with browser-launch override', () => {
|
||||
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
|
||||
syncBrowserMcpToConfigDir: () => true,
|
||||
}));
|
||||
mock.module('../../../config/unified-config-loader', () => ({
|
||||
mock.module('../../../config/config-loader-facade', () => ({
|
||||
getBrowserConfig: () => makeBrowserConfig(true, 'auto'),
|
||||
loadOrCreateUnifiedConfig: () => ({}),
|
||||
getThinkingConfig: () => ({}),
|
||||
@@ -98,7 +98,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
|
||||
});
|
||||
|
||||
it('emits warn() when getBlockedBrowserOverrideWarning returns a message', async () => {
|
||||
mock.module('../../utils/browser', () => ({
|
||||
mock.module('../../../utils/browser', () => ({
|
||||
appendBrowserToolArgs: (a: string[]) => a,
|
||||
resolveBrowserLaunchFlagResolution: (args: string[]) => ({
|
||||
override: 'force-enable' as const,
|
||||
@@ -111,7 +111,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
|
||||
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
|
||||
syncBrowserMcpToConfigDir: () => true,
|
||||
}));
|
||||
mock.module('../../../config/unified-config-loader', () => ({
|
||||
mock.module('../../../config/config-loader-facade', () => ({
|
||||
getBrowserConfig: () => makeBrowserConfig(false, 'never'),
|
||||
loadOrCreateUnifiedConfig: () => ({}),
|
||||
getThinkingConfig: () => ({}),
|
||||
@@ -128,7 +128,7 @@ describe('resolveBrowserLaunchFlags — blocked override warning', () => {
|
||||
|
||||
describe('resolveBrowserRuntime — attach disabled', () => {
|
||||
it('returns undefined browserRuntimeEnv when browser attach is disabled', async () => {
|
||||
mock.module('../../utils/browser', () => ({
|
||||
mock.module('../../../utils/browser', () => ({
|
||||
appendBrowserToolArgs: (a: string[]) => a,
|
||||
resolveBrowserLaunchFlagResolution: (a: string[]) => ({
|
||||
override: undefined,
|
||||
@@ -141,7 +141,7 @@ describe('resolveBrowserRuntime — attach disabled', () => {
|
||||
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: undefined }),
|
||||
syncBrowserMcpToConfigDir: () => true,
|
||||
}));
|
||||
mock.module('../../../config/unified-config-loader', () => ({
|
||||
mock.module('../../../config/config-loader-facade', () => ({
|
||||
getBrowserConfig: () => makeBrowserConfig(false),
|
||||
loadOrCreateUnifiedConfig: () => ({}),
|
||||
getThinkingConfig: () => ({}),
|
||||
@@ -158,7 +158,7 @@ describe('resolveBrowserRuntime — attach disabled', () => {
|
||||
describe('resolveBrowserRuntime — active runtime env', () => {
|
||||
it('returns runtimeEnv when browser attach resolves successfully', async () => {
|
||||
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,
|
||||
resolveBrowserLaunchFlagResolution: (a: string[]) => ({
|
||||
override: 'force-enable' as const,
|
||||
@@ -171,7 +171,7 @@ describe('resolveBrowserRuntime — active runtime env', () => {
|
||||
resolveOptionalBrowserAttachRuntime: async () => ({ runtimeEnv: fakeRuntimeEnv }),
|
||||
syncBrowserMcpToConfigDir: () => true,
|
||||
}));
|
||||
mock.module('../../../config/unified-config-loader', () => ({
|
||||
mock.module('../../../config/config-loader-facade', () => ({
|
||||
getBrowserConfig: () => makeBrowserConfig(true, 'always'),
|
||||
loadOrCreateUnifiedConfig: () => ({}),
|
||||
getThinkingConfig: () => ({}),
|
||||
|
||||
@@ -34,7 +34,7 @@ mock.module('child_process', () => ({
|
||||
const mockEscapeShellArg = jest.fn((s: string) => `"${s}"`);
|
||||
const mockGetWindowsEscapedCommandShell = jest.fn().mockReturnValue('cmd.exe');
|
||||
|
||||
mock.module('../../utils/shell-executor', () => ({
|
||||
mock.module('../../../utils/shell-executor', () => ({
|
||||
escapeShellArg: mockEscapeShellArg,
|
||||
getWindowsEscapedCommandShell: mockGetWindowsEscapedCommandShell,
|
||||
}));
|
||||
@@ -46,7 +46,7 @@ mock.module('../../config/config-generator', () => ({
|
||||
getProviderConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
mock.module('../../utils/websearch-manager', () => ({
|
||||
mock.module('../../../utils/websearch-manager', () => ({
|
||||
appendThirdPartyWebSearchToolArgs: (args: string[]) => args,
|
||||
createWebSearchTraceContext: jest.fn().mockReturnValue({}),
|
||||
ensureWebSearchMcpOrThrow: jest.fn(),
|
||||
@@ -54,17 +54,17 @@ mock.module('../../utils/websearch-manager', () => ({
|
||||
getWebSearchHookEnv: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
mock.module('../../utils/image-analysis', () => ({
|
||||
mock.module('../../../utils/image-analysis', () => ({
|
||||
appendThirdPartyImageAnalysisToolArgs: (args: string[]) => [...args, '--mcp-image-analysis'],
|
||||
syncImageAnalysisMcpToConfigDir: jest.fn(),
|
||||
ensureImageAnalysisMcpOrThrow: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
mock.module('../../utils/browser', () => ({
|
||||
mock.module('../../../utils/browser', () => ({
|
||||
appendBrowserToolArgs: (args: string[]) => [...args, '--browser'],
|
||||
}));
|
||||
|
||||
mock.module('../accounts/account-manager', () => ({
|
||||
mock.module('../../accounts/account-manager', () => ({
|
||||
getDefaultAccount: jest.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
@@ -82,7 +82,7 @@ mock.module('../account-resolution', () => ({
|
||||
}));
|
||||
|
||||
// Dynamic import for quota-manager
|
||||
mock.module('../quota/quota-manager', () => ({
|
||||
mock.module('../../quota/quota-manager', () => ({
|
||||
startQuotaMonitor: jest.fn(),
|
||||
stopQuotaMonitor: jest.fn(),
|
||||
}));
|
||||
@@ -202,7 +202,7 @@ describe('launchClaude', () => {
|
||||
|
||||
it('uses shell mode for .cmd executables on Windows', async () => {
|
||||
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)
|
||||
expect(spawnOpts.shell).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 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 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,26 +23,21 @@ const mockGetSuggestedReplacementModel = jest
|
||||
.fn<string | undefined, [string, string]>()
|
||||
.mockReturnValue(undefined);
|
||||
|
||||
jest.mock('../model-warnings', () => {
|
||||
// We test the real implementation — only mock its dependencies
|
||||
return jest.requireActual('../model-warnings');
|
||||
});
|
||||
|
||||
jest.mock('../../config/model-config', () => ({
|
||||
mock.module('../../config/model-config', () => ({
|
||||
getCurrentModel: mockGetCurrentModel,
|
||||
}));
|
||||
|
||||
jest.mock('../../cliproxy/model-catalog', () => ({
|
||||
mock.module('../../model-catalog', () => ({
|
||||
isModelBroken: mockIsModelBroken,
|
||||
getModelIssueUrl: mockGetModelIssueUrl,
|
||||
findModel: mockFindModel,
|
||||
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';
|
||||
|
||||
const { warnBrokenModels } = await import('../model-warnings');
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeSimpleCfg(overrides: Partial<ExecutorConfig> = {}): ExecutorConfig {
|
||||
|
||||
@@ -150,6 +150,7 @@ export async function resolveAccounts(
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
return { earlyExit: true };
|
||||
}
|
||||
setDefaultAccount(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(` Run "ccs ${provider} --auth" to add an account first`);
|
||||
process.exit(1);
|
||||
return { earlyExit: true };
|
||||
}
|
||||
try {
|
||||
const success = renameAccount(provider, defaultAccount.id, setNickname);
|
||||
@@ -174,12 +176,15 @@ export async function resolveAccounts(
|
||||
} else {
|
||||
console.error(fail('Failed to rename account'));
|
||||
process.exit(1);
|
||||
return { earlyExit: true };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(fail(err instanceof Error ? err.message : 'Failed to rename account'));
|
||||
process.exit(1);
|
||||
return { earlyExit: true };
|
||||
}
|
||||
process.exit(0);
|
||||
return { earlyExit: true };
|
||||
}
|
||||
|
||||
return { earlyExit: false };
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
* by a direct atomic write; the running-proxy PATCH path is covered at the
|
||||
* 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 os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { handleOrderSubcommand } from '../order-subcommand';
|
||||
mock.module('../../../cliproxy/proxy/proxy-detector', () => ({
|
||||
detectRunningProxy: async () => ({ running: false, verified: false }),
|
||||
}));
|
||||
|
||||
describe('handleOrderSubcommand', () => {
|
||||
let tempHome: string;
|
||||
@@ -52,6 +54,13 @@ describe('handleOrderSubcommand', () => {
|
||||
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(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-order-subcommand-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
@@ -86,7 +95,7 @@ describe('handleOrderSubcommand', () => {
|
||||
registerAccount('claude', 'claude-a.json', 'a@x.com');
|
||||
registerAccount('claude', 'claude-b.json', 'b@x.com');
|
||||
|
||||
await handleOrderSubcommand(['claude']);
|
||||
await runOrderSubcommand(['claude']);
|
||||
|
||||
const output = lines.join('\n');
|
||||
// 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-b.json', 'b@x.com');
|
||||
|
||||
await handleOrderSubcommand(['claude']);
|
||||
await runOrderSubcommand(['claude']);
|
||||
|
||||
const output = lines.join('\n');
|
||||
expect(output).toContain('no priority set');
|
||||
@@ -131,7 +140,7 @@ describe('handleOrderSubcommand', () => {
|
||||
registerAccount('claude', 'claude-b.json', '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).
|
||||
expect('priority' in readAuthFile('claude-a.json')).toBe(false);
|
||||
@@ -149,7 +158,7 @@ describe('handleOrderSubcommand', () => {
|
||||
const { registerAccount } = await registerClaude();
|
||||
registerAccount('claude', 'claude-a.json', 'a@x.com');
|
||||
|
||||
await handleOrderSubcommand(['claude', '--reset']);
|
||||
await runOrderSubcommand(['claude', '--reset']);
|
||||
|
||||
const output = lines.join('\n');
|
||||
expect(output).toContain('reset to file order');
|
||||
|
||||
@@ -231,6 +231,7 @@ export class DeltaAccumulator {
|
||||
if (usage) {
|
||||
this.inputTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
||||
this.outputTokens = usage.completion_tokens || usage.output_tokens || 0;
|
||||
this.usageReceived = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const GlmtTransformer = require('../dist/glmt/glmt-transformer').default;
|
||||
const { DeltaAccumulator } = require('../dist/glmt/delta-accumulator');
|
||||
const GlmtTransformer = require('../../dist/glmt/glmt-transformer').default;
|
||||
const { DeltaAccumulator } = require('../../dist/glmt/delta-accumulator');
|
||||
|
||||
/**
|
||||
* Token Counting Validation Tests
|
||||
@@ -60,8 +60,8 @@ function assertEqual(actual, expected, message) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`${message || 'Assertion failed'}\n` +
|
||||
` Expected: ${JSON.stringify(expected)}\n` +
|
||||
` Actual: ${JSON.stringify(actual)}`
|
||||
` Expected: ${JSON.stringify(expected)}\n` +
|
||||
` Actual: ${JSON.stringify(actual)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,8 @@ function assertDeepEqual(actual, expected, message) {
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message || 'Deep equality failed'}\n` +
|
||||
` Expected: ${expectedStr}\n` +
|
||||
` Actual: ${actualStr}`
|
||||
` Expected: ${expectedStr}\n` +
|
||||
` Actual: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,14 +102,14 @@ runner.test('message_delta includes both input_tokens and output_tokens', () =>
|
||||
// Simulate usage data
|
||||
accumulator.updateUsage({
|
||||
prompt_tokens: 150,
|
||||
completion_tokens: 75
|
||||
completion_tokens: 75,
|
||||
});
|
||||
accumulator.finishReason = 'stop';
|
||||
|
||||
const events = transformer.finalizeDelta(accumulator);
|
||||
|
||||
// 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.data.usage, 'usage should exist in message_delta');
|
||||
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 = {
|
||||
id: 'chatcmpl-123',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Simple response'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Simple response',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 5,
|
||||
total_tokens: 15
|
||||
}
|
||||
total_tokens: 15,
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
@@ -153,26 +155,30 @@ runner.test('token counting with tool calls', () => {
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-456',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
arguments: '{"location":"London"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
arguments: '{"location":"London"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls',
|
||||
},
|
||||
finish_reason: 'tool_calls'
|
||||
}],
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 25,
|
||||
total_tokens: 75
|
||||
}
|
||||
total_tokens: 75,
|
||||
},
|
||||
};
|
||||
|
||||
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.output_tokens, 25, 'output_tokens should be 25');
|
||||
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 = {
|
||||
id: 'chatcmpl-789',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Let me analyze this request...',
|
||||
content: 'I need to call a tool',
|
||||
tool_calls: [{
|
||||
id: 'call_2',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'calculate',
|
||||
arguments: '{"expression":"2+2"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Let me analyze this request...',
|
||||
content: 'I need to call a tool',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_2',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'calculate',
|
||||
arguments: '{"expression":"2+2"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls',
|
||||
},
|
||||
finish_reason: 'tool_calls'
|
||||
}],
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 100,
|
||||
completion_tokens: 80,
|
||||
total_tokens: 180
|
||||
}
|
||||
total_tokens: 180,
|
||||
},
|
||||
};
|
||||
|
||||
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');
|
||||
assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100');
|
||||
assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80');
|
||||
assertTrue(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');
|
||||
assertTrue(
|
||||
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 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {},
|
||||
finish_reason: 'stop'
|
||||
}]
|
||||
}
|
||||
choices: [
|
||||
{
|
||||
delta: {},
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
||||
|
||||
// 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(accumulator.finalized, false, 'accumulator should NOT be finalized');
|
||||
|
||||
@@ -262,15 +283,15 @@ runner.test('deferred finalization waits for usage data', () => {
|
||||
data: {
|
||||
usage: {
|
||||
prompt_tokens: 200,
|
||||
completion_tokens: 100
|
||||
}
|
||||
}
|
||||
completion_tokens: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
||||
|
||||
// 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(accumulator.finalized, true, 'accumulator should be finalized');
|
||||
assertEqual(accumulator.usageReceived, true, 'usageReceived should be true');
|
||||
@@ -290,9 +311,9 @@ runner.test('finalization waits for BOTH finish_reason AND usage', () => {
|
||||
data: {
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 30
|
||||
}
|
||||
}
|
||||
completion_tokens: 30,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
||||
@@ -303,18 +324,20 @@ runner.test('finalization waits for BOTH finish_reason AND usage', () => {
|
||||
const openaiEvent2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {},
|
||||
finish_reason: 'stop'
|
||||
}]
|
||||
}
|
||||
choices: [
|
||||
{
|
||||
delta: {},
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
||||
assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set');
|
||||
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');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_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
|
||||
const doneEvent = {
|
||||
event: 'done'
|
||||
event: 'done',
|
||||
};
|
||||
|
||||
const events = transformer.transformDelta(doneEvent, accumulator);
|
||||
|
||||
// Should finalize with zero tokens (graceful degradation)
|
||||
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');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_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',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
delta: { role: 'assistant' }
|
||||
}]
|
||||
}
|
||||
choices: [
|
||||
{
|
||||
delta: { role: 'assistant' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
transformer.transformDelta(event1, accumulator);
|
||||
|
||||
@@ -369,25 +394,25 @@ runner.test('no regression: thinking blocks still work', () => {
|
||||
const event2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
reasoning_content: 'Analyzing the problem...'
|
||||
}
|
||||
}]
|
||||
}
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
reasoning_content: 'Analyzing the problem...',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const events2 = transformer.transformDelta(event2, accumulator);
|
||||
|
||||
// Check thinking block was created
|
||||
const hasThinkingStart = events2.some(e =>
|
||||
e.event === 'content_block_start' &&
|
||||
e.data.content_block.type === 'thinking'
|
||||
const hasThinkingStart = events2.some(
|
||||
(e) => e.event === 'content_block_start' && e.data.content_block.type === 'thinking'
|
||||
);
|
||||
assertEqual(hasThinkingStart, true, 'thinking block should start');
|
||||
|
||||
const hasThinkingDelta = events2.some(e =>
|
||||
e.event === 'content_block_delta' &&
|
||||
e.data.delta.type === 'thinking_delta'
|
||||
const hasThinkingDelta = events2.some(
|
||||
(e) => e.event === 'content_block_delta' && e.data.delta.type === 'thinking_delta'
|
||||
);
|
||||
assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted');
|
||||
});
|
||||
@@ -404,34 +429,36 @@ runner.test('no regression: tool calls execute correctly', () => {
|
||||
const event = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_abc',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search',
|
||||
arguments: '{"q":"test"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: 'call_abc',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search',
|
||||
arguments: '{"q":"test"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const events = transformer.transformDelta(event, accumulator);
|
||||
|
||||
const toolUseStart = events.find(e =>
|
||||
e.event === 'content_block_start' &&
|
||||
e.data.content_block.type === 'tool_use'
|
||||
const toolUseStart = events.find(
|
||||
(e) => e.event === 'content_block_start' && e.data.content_block.type === 'tool_use'
|
||||
);
|
||||
assertExists(toolUseStart, 'tool_use block should start');
|
||||
assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search');
|
||||
|
||||
const inputJsonDelta = events.find(e =>
|
||||
e.event === 'content_block_delta' &&
|
||||
e.data.delta.type === 'input_json_delta'
|
||||
const inputJsonDelta = events.find(
|
||||
(e) => e.event === 'content_block_delta' && e.data.delta.type === 'input_json_delta'
|
||||
);
|
||||
assertExists(inputJsonDelta, 'input_json_delta should be emitted');
|
||||
});
|
||||
@@ -448,32 +475,41 @@ runner.test('no regression: streaming still works', () => {
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{ delta: { role: 'assistant' } }]
|
||||
}
|
||||
choices: [{ delta: { role: 'assistant' } }],
|
||||
},
|
||||
};
|
||||
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
|
||||
const event2 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: 'Hello' } }]
|
||||
}
|
||||
choices: [{ delta: { content: 'Hello' } }],
|
||||
},
|
||||
};
|
||||
const events2 = transformer.transformDelta(event2, accumulator);
|
||||
assertTrue(events2.some(e => e.event === 'content_block_start'), 'content_block_start');
|
||||
assertTrue(events2.some(e => e.event === 'content_block_delta'), 'content_block_delta');
|
||||
assertTrue(
|
||||
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
|
||||
const event3 = {
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: ' world' } }]
|
||||
}
|
||||
choices: [{ delta: { content: ' world' } }],
|
||||
},
|
||||
};
|
||||
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');
|
||||
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 = {
|
||||
id: 'chatcmpl-buffered',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Thinking step by step...',
|
||||
content: 'Final answer'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'Thinking step by step...',
|
||||
content: 'Final answer',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 20,
|
||||
completion_tokens: 15,
|
||||
total_tokens: 35
|
||||
}
|
||||
total_tokens: 35,
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertEqual(result.type, 'message', 'type should be message');
|
||||
assertEqual(result.role, 'assistant', 'role should be assistant');
|
||||
assertTrue(result.content.some(b => b.type === 'thinking'), 'has thinking');
|
||||
assertTrue(result.content.some(b => b.type === 'text'), 'has text');
|
||||
assertTrue(
|
||||
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.output_tokens, 15, 'output_tokens');
|
||||
});
|
||||
@@ -522,7 +566,7 @@ runner.test('usageReceived flag is set when usage data arrives', () => {
|
||||
|
||||
accumulator.updateUsage({
|
||||
prompt_tokens: 100,
|
||||
completion_tokens: 50
|
||||
completion_tokens: 50,
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// Message start
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{ delta: { role: 'assistant' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
model: 'GLM-4.6',
|
||||
choices: [{ delta: { role: 'assistant' } }],
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// Thinking
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { reasoning_content: 'Thinking...' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { reasoning_content: 'Thinking...' } }],
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// Text
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: 'Answer' } }]
|
||||
}
|
||||
}, accumulator);
|
||||
transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: { content: 'Answer' } }],
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// Tool call
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'tool', arguments: '{}' }
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, accumulator);
|
||||
transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'tool', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// Usage arrives
|
||||
transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
usage: { prompt_tokens: 300, completion_tokens: 200 }
|
||||
}
|
||||
}, accumulator);
|
||||
transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
usage: { prompt_tokens: 300, completion_tokens: 200 },
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// finish_reason arrives
|
||||
const finalEvents = transformer.transformDelta({
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: {}, finish_reason: 'tool_calls' }]
|
||||
}
|
||||
}, accumulator);
|
||||
const finalEvents = transformer.transformDelta(
|
||||
{
|
||||
event: 'data',
|
||||
data: {
|
||||
choices: [{ delta: {}, finish_reason: 'tool_calls' }],
|
||||
},
|
||||
},
|
||||
accumulator
|
||||
);
|
||||
|
||||
// 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');
|
||||
assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300');
|
||||
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
|
||||
runner.run().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
}).catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
runner
|
||||
.run()
|
||||
.then((success) => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -7,10 +7,9 @@ const API_KEY = process.env.Z_AI_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
|
||||
const MODEL = 'GLM-4.6';
|
||||
|
||||
if (!API_KEY || API_KEY === 'your-api-key-here') {
|
||||
console.error('[ERROR] Z.AI API key not found');
|
||||
console.error('[INFO] Set Z_AI_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable');
|
||||
console.error('[INFO] Example: export Z_AI_API_KEY=your-key-here');
|
||||
process.exit(1);
|
||||
console.log('[SKIP] Z.AI API key not found');
|
||||
console.log('[INFO] Set Z_AI_API_KEY or ANTHROPIC_AUTH_TOKEN to run this external smoke test');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Test request with reasoning
|
||||
@@ -19,14 +18,15 @@ const requestBody = JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
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,
|
||||
reasoning: true,
|
||||
reasoning_effort: 'medium',
|
||||
max_tokens: 4096,
|
||||
do_sample: true
|
||||
do_sample: true,
|
||||
});
|
||||
|
||||
const options = {
|
||||
@@ -36,10 +36,10 @@ const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
'User-Agent': 'CCS-GLMT-StreamingTest/1.0'
|
||||
}
|
||||
'User-Agent': 'CCS-GLMT-StreamingTest/1.0',
|
||||
},
|
||||
};
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════');
|
||||
@@ -63,7 +63,7 @@ const req = https.request(options, (res) => {
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('data', (chunk) => (errorBody += chunk));
|
||||
res.on('end', () => {
|
||||
console.error('[ERROR] Request failed:', res.statusCode, res.statusMessage);
|
||||
console.error('[ERROR] Response:', errorBody);
|
||||
@@ -98,7 +98,7 @@ const req = https.request(options, (res) => {
|
||||
// Keep incomplete line in buffer
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
lines.forEach(line => {
|
||||
lines.forEach((line) => {
|
||||
if (line.startsWith('data: ')) {
|
||||
eventCount++;
|
||||
const data = line.substring(6);
|
||||
@@ -124,7 +124,10 @@ const req = https.request(options, (res) => {
|
||||
console.log(`[EVENT ${eventCount}] *** REASONING IN DELTA ***`);
|
||||
console.log(' Delta keys:', Object.keys(delta).join(', '));
|
||||
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('');
|
||||
}
|
||||
|
||||
@@ -134,7 +137,10 @@ const req = https.request(options, (res) => {
|
||||
console.log(`[EVENT ${eventCount}] *** REASONING IN MESSAGE (COMPLETE) ***`);
|
||||
console.log(' Message keys:', Object.keys(message).join(', '));
|
||||
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('');
|
||||
}
|
||||
|
||||
@@ -159,7 +165,6 @@ const req = https.request(options, (res) => {
|
||||
console.log(`[EVENT ${eventCount}] Usage:`, JSON.stringify(parsed.usage));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(`[EVENT ${eventCount}] [PARSE ERROR]`, e.message);
|
||||
console.log(' Raw:', data.substring(0, 100));
|
||||
|
||||
@@ -10,7 +10,12 @@ const GlmtTransformer = require('../../../dist/glmt/glmt-transformer').default;
|
||||
* 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
|
||||
console.log('Cleaning up existing logs...');
|
||||
@@ -20,12 +25,12 @@ if (fs.existsSync(logDir)) {
|
||||
|
||||
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}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test without debug' }]
|
||||
messages: [{ role: 'user', content: 'Test without debug' }],
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
@@ -33,15 +38,17 @@ console.log('\n=== Test 1: Debug Mode OFF (default) ===');
|
||||
const openaiResponse = {
|
||||
id: 'test-1',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response without debug',
|
||||
reasoning_content: 'Some reasoning...'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
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, {});
|
||||
@@ -59,13 +66,13 @@ console.log('\n=== Test 1: Debug Mode OFF (default) ===');
|
||||
|
||||
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(`Log directory: ${transformer.debugLogDir}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test with debug' }]
|
||||
messages: [{ role: 'user', content: 'Test with debug' }],
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
@@ -73,15 +80,17 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
|
||||
const openaiResponse = {
|
||||
id: 'test-2',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response with debug',
|
||||
reasoning_content: 'Detailed reasoning process for debugging...'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
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, {});
|
||||
@@ -105,10 +114,10 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
|
||||
}
|
||||
|
||||
// Check file names
|
||||
const hasRequestAnthropic = files.some(f => f.includes('request-anthropic'));
|
||||
const hasRequestOpenai = files.some(f => f.includes('request-openai'));
|
||||
const hasResponseOpenai = files.some(f => f.includes('response-openai'));
|
||||
const hasResponseAnthropic = files.some(f => f.includes('response-anthropic'));
|
||||
const hasRequestAnthropic = files.some((f) => f.includes('request-anthropic'));
|
||||
const hasRequestOpenai = files.some((f) => f.includes('request-openai'));
|
||||
const hasResponseOpenai = files.some((f) => f.includes('response-openai'));
|
||||
const hasResponseAnthropic = files.some((f) => f.includes('response-anthropic'));
|
||||
|
||||
console.log(`\nFile types found:`);
|
||||
console.log(` request-anthropic: ${hasRequestAnthropic ? '✓' : '✗'}`);
|
||||
@@ -122,25 +131,27 @@ console.log('\n=== Test 2: Debug Mode ON (via config) ===');
|
||||
}
|
||||
|
||||
// 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 responseOpenaiData = JSON.parse(fs.readFileSync(responseOpenaiPath, 'utf8'));
|
||||
|
||||
console.log(`\nChecking response-openai.json for reasoning_content...`);
|
||||
if (responseOpenaiData.choices[0].message.reasoning_content) {
|
||||
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 {
|
||||
console.log('ERROR: reasoning_content NOT found in response-openai.json!');
|
||||
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 responseAnthropicData = JSON.parse(fs.readFileSync(responseAnthropicPath, 'utf8'));
|
||||
|
||||
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) {
|
||||
console.log('✓ Thinking block found in response-anthropic.json');
|
||||
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 });
|
||||
|
||||
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}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test with env var' }]
|
||||
messages: [{ role: 'user', content: 'Test with env var' }],
|
||||
};
|
||||
|
||||
transformer.transformRequest(input);
|
||||
@@ -171,15 +182,17 @@ console.log('\n=== Test 3: Debug Mode via CCS_DEBUG=1 ===');
|
||||
const openaiResponse = {
|
||||
id: 'test-3',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response',
|
||||
reasoning_content: 'Reasoning...'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
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, {});
|
||||
@@ -211,14 +224,14 @@ console.log('\n=== Test 4: Error Handling (No Write Permission) ===');
|
||||
const transformer = new GlmtTransformer({
|
||||
debugLog: true,
|
||||
debugLogDir: readOnlyDir,
|
||||
verbose: false
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
console.log('Testing graceful error handling with read-only directory...');
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test error handling' }]
|
||||
messages: [{ role: 'user', content: 'Test error handling' }],
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,28 +2,35 @@
|
||||
'use strict';
|
||||
|
||||
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');
|
||||
|
||||
const iterations = 1000;
|
||||
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-performance-test-'));
|
||||
const logDir = path.join(testRoot, 'logs');
|
||||
|
||||
const sampleRequest = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test performance' }]
|
||||
messages: [{ role: 'user', content: 'Test performance' }],
|
||||
};
|
||||
|
||||
const sampleResponse = {
|
||||
id: 'perf-test',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Quick response',
|
||||
reasoning_content: 'Some reasoning content here...'
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
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
|
||||
@@ -41,7 +48,7 @@ console.log(` Average per request: ${(timeOff / iterations).toFixed(2)}ms`);
|
||||
|
||||
// Test 2: Debug ON
|
||||
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();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
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).`);
|
||||
|
||||
// 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)) {
|
||||
fs.rmSync(logDir, { recursive: true, force: true });
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user