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