Merge pull request #1520 from kaitranntt/kai/fix/1516-test-bucket-paths

fix(test): run bucket paths explicitly
This commit is contained in:
Kai (Tam Nhu) Tran
2026-06-14 11:59:48 -04:00
committed by GitHub
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
// 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');
+1
View File
@@ -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;
}
}
+253 -184
View File
@@ -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);
});
+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';
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));
+54 -41
View File
@@ -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 {
+18 -14
View File
@@ -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);
});