mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
Harden test isolation for smoke stability (#1192)
* Fix flaky smoke build checks Replace feature-flag build preprocessing with a Bun onLoad transform so smoke/build no longer rewrites tracked src files while tests may be reading them. Keep telemetry stubs ahead of the feature transform in both CLI and SDK builds, preserve non-empty text token counts in hybrid context splitting, and make the corrupted Orama stress test assert against the actual project directory used by the test. Verified with bun run smoke, bun test src/utils/hybridContextStrategy.test.ts, bun test src/utils/knowledgeGraph.stress.test.ts --rerun-each 3, and bun test --max-concurrency=1. * Harden KnowledgeGraph smoke stress isolation Give each KnowledgeGraph stress test its own temporary config directory and remove it during teardown so Orama, SQLite, and corrupted-file state cannot bleed between stress cases or later PR test runs. Reviewed at least 20 open PRs and found the recurring smoke-and-tests failure cluster is the full unit suite, especially KnowledgeGraph corrupted Orama recovery. Verified with bun test src/utils/knowledgeGraph.stress.test.ts --rerun-each 5, bun test --max-concurrency=1, and bun run smoke. * Harden smoke test isolation Audit and harden broad smoke-adjacent test suites for process-global leaks, including env/config restoration, shared registry/module mock cleanup, fetch/axios/mock restoration, and global MACRO/platform/sandbox mutations. Replace fragile render sleeps in interactive tests with output-driven waits, and isolate provider/model/profile tests behind the shared mutation lock so unrelated PRs do not inherit stale process state. Make SQLite knowledge graph cleanup clear closed on-disk databases before best-effort file cleanup, with coverage for the stale database reset path. Verified: bun run smoke; bun test --max-concurrency=1; python -m pytest -q python/tests; bun run security:pr-scan -- --base origin/main; bun run test:provider; npm run test:provider-recommendation. * Harden test isolation across smoke suite Guard process-global test mutations with the shared mutation lock across env, module mock, config cache, and storage tests.\n\nDeep-copy global config snapshots, restore transient globals precisely, and make plugin/LSP mocks expose compatible export surfaces so concurrent test loading does not poison unrelated suites.\n\nReplace fixed SDK cleanup sleeps with call polling to remove timing sensitivity.\n\nVerification:\n- bun test --max-concurrency=1\n- bun run smoke\n- python -m pytest -q python/tests\n- bun run test:provider\n- npm run test:provider-recommendation\n- bun run security:pr-scan -- --base origin/main\n- git diff --check * Close remaining test global-state leaks Guard remaining cache, plugin, console, and VS Code module-mock tests with the shared mutation lock.\n\nThis follow-up audit covers non-env process-global state that can leak across test files: tool schema cache, cache stats tracker state, plugin loader caches, console.error replacement, and VS Code mock.module usage.\n\nVerification:\n- leak-surface scans for env/global/mock.module/cache outliers\n- duplicate top-level mock collision cluster\n- affected tests cluster\n- bun test --max-concurrency=1\n- bun run smoke\n- git diff --check * Guard remaining mock restore cleanup Lock tests that call bun:test mock.restore without installing module mocks themselves.\n\nmock.restore is process-global, so these cleanup hooks can still tear down another test file's active module mocks when files run concurrently.\n\nVerification:\n- expanded leak scans for env, globals, module mocks, mock.restore, timers, argv, and caches\n- bun test src/components/useCodexOAuthFlow.test.tsx src/services/github/deviceFlow.test.ts\n- bun test --max-concurrency=1\n- bun run smoke\n- git diff --check * Harden test isolation for smoke stability Serialize tests that mutate process-global state behind the shared mutation lock, including process.env, transient globals, global config/cache state, storage mocks, and Bun module mocks. Add isolated env mutex instances for SDK mutex tests so timeout coverage no longer manipulates the live process-global mutex. Move top-level mock.module setup behind lock acquisition and restore mocks before releasing locks to prevent cross-file leakage under parallel smoke runs. Verified with: bun test --max-concurrency=1; bun test; bun run smoke.
This commit is contained in:
+32
-45
@@ -8,8 +8,7 @@
|
||||
* - src/ path aliases
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
import { noTelemetryPlugin } from './no-telemetry-plugin'
|
||||
import { CLI_EXTERNALS, SDK_EXTERNALS } from './externals.js'
|
||||
|
||||
@@ -69,53 +68,42 @@ const featureFlags: Record<string, boolean> = {
|
||||
// so the previous onResolve/onLoad shim was silently ineffective — ALL
|
||||
// feature() calls evaluated to false regardless of the featureFlags map.
|
||||
//
|
||||
// Fix: pre-process source files to strip the bun:bundle import and
|
||||
// replace feature('FLAG') calls with their boolean literal. Files are
|
||||
// modified in-place before Bun.build() and restored in a finally block.
|
||||
// Fix: transform source as Bun loads each module, stripping the bun:bundle
|
||||
// import and replacing feature('FLAG') calls with their boolean literal.
|
||||
// The working tree stays immutable while smoke/build runs.
|
||||
|
||||
// Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n)
|
||||
const featureCallRe = /\bfeature\(\s*['"](\w+)['"][,\s]*\)/gs
|
||||
const featureImportRe = /import\s*\{[^}]*\bfeature\b[^}]*\}\s*from\s*['"]bun:bundle['"];?\s*\n?/g
|
||||
const modifiedFiles = new Map<string, string>() // path → original content
|
||||
const featureFlagTransformedFiles = new Set<string>()
|
||||
|
||||
function preProcessFeatureFlags(dir: string) {
|
||||
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, ent.name)
|
||||
if (ent.isDirectory()) { preProcessFeatureFlags(full); continue }
|
||||
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
||||
const featureFlagPreprocessPlugin = {
|
||||
name: 'feature-flag-preprocess',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.[cm]?tsx?$/ }, args => {
|
||||
const normalizedPath = args.path.replace(/\\/g, '/')
|
||||
if (!normalizedPath.includes('/src/')) return null
|
||||
|
||||
const raw = readFileSync(full, 'utf-8')
|
||||
if (!raw.includes('feature(')) continue
|
||||
const raw = readFileSync(args.path, 'utf-8')
|
||||
if (!raw.includes('feature(')) return null
|
||||
|
||||
let contents = raw
|
||||
contents = contents.replace(featureImportRe, '')
|
||||
contents = contents.replace(featureCallRe, (_match, name) =>
|
||||
String((featureFlags as Record<string, boolean>)[name] ?? false),
|
||||
)
|
||||
let contents = raw
|
||||
contents = contents.replace(featureImportRe, '')
|
||||
contents = contents.replace(featureCallRe, (_match, name) =>
|
||||
String((featureFlags as Record<string, boolean>)[name] ?? false),
|
||||
)
|
||||
|
||||
if (contents !== raw) {
|
||||
modifiedFiles.set(full, raw)
|
||||
writeFileSync(full, contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contents === raw) return null
|
||||
|
||||
function restoreModifiedFiles() {
|
||||
for (const [path, original] of modifiedFiles) {
|
||||
writeFileSync(path, original)
|
||||
}
|
||||
modifiedFiles.clear()
|
||||
}
|
||||
|
||||
preProcessFeatureFlags(join(import.meta.dir, '..', 'src'))
|
||||
const numModified = modifiedFiles.size
|
||||
|
||||
// Restore source files on abrupt termination (Ctrl+C, kill, etc.)
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||
process.on(signal, () => {
|
||||
restoreModifiedFiles()
|
||||
process.exit(signal === 'SIGINT' ? 130 : 143)
|
||||
})
|
||||
featureFlagTransformedFiles.add(args.path)
|
||||
return {
|
||||
contents,
|
||||
loader: args.path.endsWith('.tsx') || args.path.endsWith('.jsx')
|
||||
? 'tsx'
|
||||
: 'ts',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
let result: Awaited<ReturnType<typeof Bun.build>> | undefined
|
||||
@@ -149,6 +137,7 @@ result = await Bun.build({
|
||||
},
|
||||
plugins: [
|
||||
noTelemetryPlugin,
|
||||
featureFlagPreprocessPlugin,
|
||||
{
|
||||
name: 'bun-bundle-shim',
|
||||
setup(build) {
|
||||
@@ -185,8 +174,7 @@ export async function handleBgFlag() { throw new Error("Background sessions are
|
||||
],
|
||||
] as const)
|
||||
|
||||
// bun:bundle feature() replacement is handled by the source
|
||||
// pre-processing step above (see preProcessFeatureFlags).
|
||||
// bun:bundle feature() replacement is handled by featureFlagPreprocessPlugin.
|
||||
// The previous onResolve/onLoad shim was ineffective in Bun
|
||||
// v1.3.9+ because the bun: namespace is resolved natively
|
||||
// before the JS plugin phase runs.
|
||||
@@ -456,6 +444,7 @@ sdkResult = await Bun.build({
|
||||
external: SDK_EXTERNALS,
|
||||
plugins: [
|
||||
noTelemetryPlugin,
|
||||
featureFlagPreprocessPlugin,
|
||||
// Stub missing internal/optional modules (same pattern as CLI build)
|
||||
{
|
||||
name: 'sdk-missing-stub',
|
||||
@@ -860,9 +849,7 @@ if (!sdkResult.success) {
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Always restore source files, even if Bun.build() throws
|
||||
restoreModifiedFiles()
|
||||
console.log(` 🔄 feature-flags: pre-processed ${numModified} files (restored)`)
|
||||
console.log(` 🔄 feature-flags: transformed ${featureFlagTransformedFiles.size} files during bundling`)
|
||||
}
|
||||
|
||||
// ── Validate SDK bundle for React/Ink leakage ──────────────────────────────
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { mkdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { acquireEnvMutex, releaseEnvMutex } from '../src/entrypoints/sdk/shared.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup: dynamically import the source-level growthbook no-op stub.
|
||||
// The stub reads ~/.claude/feature-flags.json for local flag overrides.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const originalFlagsFile = process.env.CLAUDE_FEATURE_FLAGS_FILE
|
||||
const testDir = join(tmpdir(), `growthbook-stub-test-${process.pid}`)
|
||||
const flagsFile = join(testDir, 'test-flags.json')
|
||||
let envLockAcquired = false
|
||||
let stub: typeof import('../src/services/analytics/growthbook.js')
|
||||
|
||||
mkdirSync(testDir, { recursive: true })
|
||||
beforeAll(async () => {
|
||||
const envLock = await acquireEnvMutex()
|
||||
if (!envLock.acquired) {
|
||||
throw new Error('Failed to acquire env mutex for growthbook stub test')
|
||||
}
|
||||
envLockAcquired = true
|
||||
|
||||
// Point the stub at our test flags file before import
|
||||
process.env.CLAUDE_FEATURE_FLAGS_FILE = flagsFile
|
||||
mkdirSync(testDir, { recursive: true })
|
||||
|
||||
const stub = await import('../src/services/analytics/growthbook.js')
|
||||
// Point the stub at our test flags file before import
|
||||
process.env.CLAUDE_FEATURE_FLAGS_FILE = flagsFile
|
||||
|
||||
stub = await import('../src/services/analytics/growthbook.js')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -29,8 +41,19 @@ describe('growthbook stub — local feature flag overrides', () => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(testDir, { recursive: true, force: true })
|
||||
delete process.env.CLAUDE_FEATURE_FLAGS_FILE
|
||||
try {
|
||||
rmSync(testDir, { recursive: true, force: true })
|
||||
if (originalFlagsFile === undefined) {
|
||||
delete process.env.CLAUDE_FEATURE_FLAGS_FILE
|
||||
} else {
|
||||
process.env.CLAUDE_FEATURE_FLAGS_FILE = originalFlagsFile
|
||||
}
|
||||
} finally {
|
||||
if (envLockAcquired) {
|
||||
releaseEnvMutex()
|
||||
envLockAcquired = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ── File absent ──────────────────────────────────────────────────
|
||||
@@ -145,4 +168,4 @@ describe('growthbook stub — local feature flag overrides', () => {
|
||||
writeFileSync(flagsFile, JSON.stringify({ tengu_disable_bypass_permissions_mode: true }))
|
||||
expect(await stub.checkSecurityRestrictionGate('tengu_disable_bypass_permissions_mode')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* label padding, conditional N/A footnote, recent-rows cap) which can
|
||||
* silently regress — these snapshot tests keep it honest.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import type { CacheMetrics } from '../../services/api/cacheMetrics.js'
|
||||
import {
|
||||
_setHistoryCapForTesting,
|
||||
@@ -50,11 +54,21 @@ async function runCommand(): Promise<string> {
|
||||
return result.value
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('commands/cacheStats/cacheStats.test.ts')
|
||||
resetSessionCacheStats()
|
||||
_setHistoryCapForTesting(500)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
resetSessionCacheStats()
|
||||
_setHistoryCapForTesting(500)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('/cache-stats — empty session', () => {
|
||||
test('shows friendly "no requests yet" message', async () => {
|
||||
const value = await runCommand()
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const originalClaudeCodeNewInit = process.env.CLAUDE_CODE_NEW_INIT
|
||||
|
||||
@@ -6,13 +10,21 @@ async function importInitCommand() {
|
||||
return (await import(`./init.ts?ts=${Date.now()}-${Math.random()}`)).default
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('commands/init.test.ts')
|
||||
})
|
||||
|
||||
if (originalClaudeCodeNewInit === undefined) {
|
||||
delete process.env.CLAUDE_CODE_NEW_INIT
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_NEW_INIT = originalClaudeCodeNewInit
|
||||
afterEach(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
|
||||
if (originalClaudeCodeNewInit === undefined) {
|
||||
delete process.env.CLAUDE_CODE_NEW_INIT
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_NEW_INIT = originalClaudeCodeNewInit
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +1,53 @@
|
||||
import { describe, expect, it, beforeEach } from 'bun:test'
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
||||
import { mkdtempSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { call as knowledgeCall } from './knowledge.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { getArc, addEntity, resetArc } from '../../utils/conversationArc.js'
|
||||
import { getGlobalGraph, resetGlobalGraph } from '../../utils/knowledgeGraph.js'
|
||||
import { setClaudeConfigHomeDirForTesting } from '../../utils/envUtils.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
describe('knowledge command', () => {
|
||||
const mockContext = {} as any
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
let configDir: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('commands/knowledge.test.ts')
|
||||
configDir = mkdtempSync(join(tmpdir(), 'openclaude-knowledge-command-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
setClaudeConfigHomeDirForTesting(configDir)
|
||||
resetArc()
|
||||
resetGlobalGraph()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
resetArc()
|
||||
resetGlobalGraph()
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||
}
|
||||
setClaudeConfigHomeDirForTesting(undefined)
|
||||
} finally {
|
||||
const dirToRemove = configDir
|
||||
configDir = undefined
|
||||
try {
|
||||
if (dirToRemove) {
|
||||
rmSync(dirToRemove, { recursive: true, force: true })
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const knowledgeCallWithCapture = async (args: string) => {
|
||||
const result = await knowledgeCall(args, mockContext)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = {
|
||||
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
|
||||
@@ -36,25 +40,47 @@ function restoreEnv(key: string, value: string | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function expectModelCommandDoesNotWaitForRefresh(
|
||||
commandPromise: Promise<unknown>,
|
||||
): Promise<unknown> {
|
||||
const result = await Promise.race([
|
||||
commandPromise,
|
||||
new Promise(resolve =>
|
||||
setTimeout(() => resolve(Symbol.for('openclaude.test.timeout')), 1_000),
|
||||
),
|
||||
])
|
||||
|
||||
expect(result).not.toBe(Symbol.for('openclaude.test.timeout'))
|
||||
return result
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('commands/model/model.test.tsx')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||
restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI)
|
||||
restoreEnv('CLAUDE_CODE_USE_GITHUB', originalEnv.CLAUDE_CODE_USE_GITHUB)
|
||||
restoreEnv('CLAUDE_CODE_USE_MISTRAL', originalEnv.CLAUDE_CODE_USE_MISTRAL)
|
||||
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalEnv.CLAUDE_CODE_USE_BEDROCK)
|
||||
restoreEnv('CLAUDE_CODE_USE_VERTEX', originalEnv.CLAUDE_CODE_USE_VERTEX)
|
||||
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalEnv.CLAUDE_CODE_USE_FOUNDRY)
|
||||
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
|
||||
restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE)
|
||||
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
||||
restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY)
|
||||
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
|
||||
restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS)
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
originalEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
|
||||
)
|
||||
try {
|
||||
mock.restore()
|
||||
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||
restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI)
|
||||
restoreEnv('CLAUDE_CODE_USE_GITHUB', originalEnv.CLAUDE_CODE_USE_GITHUB)
|
||||
restoreEnv('CLAUDE_CODE_USE_MISTRAL', originalEnv.CLAUDE_CODE_USE_MISTRAL)
|
||||
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalEnv.CLAUDE_CODE_USE_BEDROCK)
|
||||
restoreEnv('CLAUDE_CODE_USE_VERTEX', originalEnv.CLAUDE_CODE_USE_VERTEX)
|
||||
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalEnv.CLAUDE_CODE_USE_FOUNDRY)
|
||||
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
|
||||
restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE)
|
||||
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
||||
restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY)
|
||||
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
|
||||
restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS)
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
originalEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
|
||||
)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('opens the model picker without awaiting local model discovery refresh', async () => {
|
||||
@@ -88,12 +114,7 @@ test('opens the model picker without awaiting local model discovery refresh', as
|
||||
|
||||
// Use a fresh module instance so per-test mocks stay local to this test.
|
||||
const { call } = await importFreshModelModule('local-discovery')
|
||||
const result = await Promise.race([
|
||||
call(() => {}, {} as never, ''),
|
||||
new Promise(resolve => setTimeout(() => resolve('timeout'), 50)),
|
||||
])
|
||||
|
||||
expect(result).not.toBe('timeout')
|
||||
await expectModelCommandDoesNotWaitForRefresh(call(() => {}, {} as never, ''))
|
||||
})
|
||||
|
||||
test('opens the model picker without awaiting descriptor-backed route refresh', async () => {
|
||||
@@ -142,12 +163,7 @@ test('opens the model picker without awaiting descriptor-backed route refresh',
|
||||
}))
|
||||
|
||||
const { call } = await importFreshModelModule('descriptor-refresh-open')
|
||||
const result = await Promise.race([
|
||||
call(() => {}, {} as never, ''),
|
||||
new Promise(resolve => setTimeout(() => resolve('timeout'), 50)),
|
||||
])
|
||||
|
||||
expect(result).not.toBe('timeout')
|
||||
await expectModelCommandDoesNotWaitForRefresh(call(() => {}, {} as never, ''))
|
||||
})
|
||||
|
||||
test('shouldAutoRefreshRouteCatalog respects discovery refresh modes', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
ProviderWizard,
|
||||
TextEntryDialog,
|
||||
} from './provider.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { createProfileFile } from '../../utils/providerProfile.js'
|
||||
|
||||
const SYNC_START = '\x1B[?2026h'
|
||||
@@ -156,31 +160,39 @@ function createTestStreams(): {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('commands/provider/provider.test.tsx')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
|
||||
if (ORIGINAL_SIMPLE_ENV === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = ORIGINAL_SIMPLE_ENV
|
||||
}
|
||||
if (ORIGINAL_SIMPLE_ENV === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = ORIGINAL_SIMPLE_ENV
|
||||
}
|
||||
|
||||
if (ORIGINAL_CODEX_API_KEY === undefined) {
|
||||
delete process.env.CODEX_API_KEY
|
||||
} else {
|
||||
process.env.CODEX_API_KEY = ORIGINAL_CODEX_API_KEY
|
||||
}
|
||||
if (ORIGINAL_CODEX_API_KEY === undefined) {
|
||||
delete process.env.CODEX_API_KEY
|
||||
} else {
|
||||
process.env.CODEX_API_KEY = ORIGINAL_CODEX_API_KEY
|
||||
}
|
||||
|
||||
if (ORIGINAL_CHATGPT_ACCOUNT_ID === undefined) {
|
||||
delete process.env.CHATGPT_ACCOUNT_ID
|
||||
} else {
|
||||
process.env.CHATGPT_ACCOUNT_ID = ORIGINAL_CHATGPT_ACCOUNT_ID
|
||||
}
|
||||
if (ORIGINAL_CHATGPT_ACCOUNT_ID === undefined) {
|
||||
delete process.env.CHATGPT_ACCOUNT_ID
|
||||
} else {
|
||||
process.env.CHATGPT_ACCOUNT_ID = ORIGINAL_CHATGPT_ACCOUNT_ID
|
||||
}
|
||||
|
||||
if (ORIGINAL_CODEX_ACCOUNT_ID === undefined) {
|
||||
delete process.env.CODEX_ACCOUNT_ID
|
||||
} else {
|
||||
process.env.CODEX_ACCOUNT_ID = ORIGINAL_CODEX_ACCOUNT_ID
|
||||
if (ORIGINAL_CODEX_ACCOUNT_ID === undefined) {
|
||||
delete process.env.CODEX_ACCOUNT_ID
|
||||
} else {
|
||||
process.env.CODEX_ACCOUNT_ID = ORIGINAL_CODEX_ACCOUNT_ID
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -244,9 +256,9 @@ test('wizard step remount prevents a typed API key from leaking into the next fi
|
||||
</AppStateProvider>,
|
||||
)
|
||||
|
||||
await Bun.sleep(25)
|
||||
await waitForOutput(getOutput, output => output.includes('API key step'))
|
||||
stdin.write('sk-secret-12345678')
|
||||
await Bun.sleep(25)
|
||||
await waitForOutput(getOutput, output => output.includes('********'))
|
||||
|
||||
root.render(
|
||||
<AppStateProvider>
|
||||
@@ -262,13 +274,14 @@ test('wizard step remount prevents a typed API key from leaking into the next fi
|
||||
</AppStateProvider>,
|
||||
)
|
||||
|
||||
await Bun.sleep(25)
|
||||
const output = await waitForOutput(
|
||||
getOutput,
|
||||
frame => frame.includes('Model step') && !frame.includes('sk-secret-12345678'),
|
||||
)
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(25)
|
||||
|
||||
const output = stripAnsi(extractLastFrame(getOutput()))
|
||||
expect(output).toContain('Model step')
|
||||
expect(output).not.toContain('sk-secret-12345678')
|
||||
})
|
||||
|
||||
@@ -87,13 +87,34 @@ async function renderFrame(node: React.ReactNode): Promise<string> {
|
||||
</AppStateProvider>,
|
||||
)
|
||||
|
||||
await Bun.sleep(50)
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(25)
|
||||
try {
|
||||
return await waitForOutput(
|
||||
getOutput,
|
||||
output => output.includes('Select login method:') || output.includes('Set up provider'),
|
||||
)
|
||||
} finally {
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
}
|
||||
}
|
||||
|
||||
return stripAnsi(extractLastFrame(getOutput()))
|
||||
async function waitForOutput(
|
||||
getOutput: () => string,
|
||||
predicate: (output: string) => boolean,
|
||||
timeoutMs = 2500,
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now()
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const output = stripAnsi(extractLastFrame(getOutput()))
|
||||
if (predicate(output)) {
|
||||
return output
|
||||
}
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for ConsoleOAuthFlow test output')
|
||||
}
|
||||
|
||||
test('login picker shows the third-party platform option', async () => {
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import { createGitHubIssueUrl } from './Feedback.tsx'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
(globalThis as { MACRO?: { VERSION?: string } }).MACRO = { VERSION: '0.1.7' }
|
||||
const originalMacro = (globalThis as { MACRO?: unknown }).MACRO
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('Feedback.test.ts')
|
||||
;(globalThis as { MACRO?: { VERSION?: string } }).MACRO = { VERSION: '0.1.7' }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (originalMacro === undefined) {
|
||||
delete (globalThis as { MACRO?: unknown }).MACRO
|
||||
} else {
|
||||
;(globalThis as { MACRO?: unknown }).MACRO = originalMacro
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('createGitHubIssueUrl omits empty feedback IDs', () => {
|
||||
const url = decodeURIComponent(
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { renderToString } from '../../utils/staticRender.js'
|
||||
|
||||
describe('PromptInputQueuedCommands', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('components/PromptInput/PromptInputQueuedCommands.test.tsx')
|
||||
mock.module('../../hooks/useCommandQueue.js', () => ({
|
||||
useCommandQueue: () => [
|
||||
{
|
||||
@@ -21,7 +26,11 @@ describe('PromptInputQueuedCommands', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows a next-turn guidance banner for queued prompt messages', async () => {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
import { createRoot } from '../ink.js'
|
||||
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
|
||||
import { AppStateProvider } from '../state/AppState.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const SYNC_START = '\x1B[?2026h'
|
||||
const SYNC_END = '\x1B[?2026l'
|
||||
@@ -502,15 +506,23 @@ async function renderProviderManagerFrame(
|
||||
return output
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('components/ProviderManager.test.tsx')
|
||||
})
|
||||
|
||||
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key as keyof typeof ORIGINAL_ENV]
|
||||
} else {
|
||||
process.env[key as keyof typeof ORIGINAL_ENV] = value
|
||||
afterEach(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
|
||||
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key as keyof typeof ORIGINAL_ENV]
|
||||
} else {
|
||||
process.env[key as keyof typeof ORIGINAL_ENV] = value
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
const actualSettings = await import('../utils/settings/settings.js')
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('StartupScreen.test.ts')
|
||||
mock.module('../utils/settings/settings.js', () => ({
|
||||
...actualSettings,
|
||||
getSettings_DEPRECATED: () => ({}),
|
||||
@@ -10,12 +15,16 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { detectProvider, printStartupScreen } from './StartupScreen.js'
|
||||
import { saveGlobalConfig } from '../utils/config.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
||||
import {
|
||||
resetSettingsCache,
|
||||
setSessionSettingsCache,
|
||||
@@ -49,6 +58,7 @@ const originalEnv: Record<string, string | undefined> = {}
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
const originalIsTTY = process.stdout.isTTY
|
||||
const originalWrite = process.stdout.write
|
||||
const originalModel = getGlobalConfig().model
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
@@ -66,7 +76,7 @@ afterEach(() => {
|
||||
resetSettingsCache()
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
model: undefined,
|
||||
model: originalModel,
|
||||
}))
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
Object.defineProperty(process.stdout, 'isTTY', {
|
||||
|
||||
@@ -74,6 +74,24 @@ function createTestStreams(): {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForOutput(
|
||||
getOutput: () => string,
|
||||
predicate: (output: string) => boolean,
|
||||
timeoutMs = 2500,
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now()
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const output = stripAnsi(extractLastFrame(getOutput()))
|
||||
if (predicate(output)) {
|
||||
return output
|
||||
}
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for TextInput test output')
|
||||
}
|
||||
|
||||
function DelayedControlledTextInput(): React.ReactNode {
|
||||
const [value, setValue] = React.useState('')
|
||||
const [cursorOffset, setCursorOffset] = React.useState(0)
|
||||
@@ -183,18 +201,18 @@ test('TextInput renders typed characters before delayed parent value commits', a
|
||||
|
||||
root.render(<DelayedControlledTextInput />)
|
||||
|
||||
await Bun.sleep(50)
|
||||
await waitForOutput(getOutput, output => output.includes('Type here...'))
|
||||
stdin.write('a')
|
||||
await Bun.sleep(25)
|
||||
stdin.write('b')
|
||||
await Bun.sleep(25)
|
||||
|
||||
const output = stripAnsi(extractLastFrame(getOutput()))
|
||||
const output = await waitForOutput(
|
||||
getOutput,
|
||||
frame => frame.includes('ab') && !frame.includes('Type here...'),
|
||||
)
|
||||
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(25)
|
||||
|
||||
expect(output).toContain('ab')
|
||||
expect(output).not.toContain('Type here...')
|
||||
@@ -217,22 +235,20 @@ test('VimTextInput preserves rapid typed characters before delayed parent value
|
||||
|
||||
root.render(<DelayedControlledVimTextInput />)
|
||||
|
||||
await Bun.sleep(50)
|
||||
await waitForOutput(getOutput, output => output.includes('Type here...'))
|
||||
stdin.write('a')
|
||||
await Bun.sleep(25)
|
||||
stdin.write('s')
|
||||
await Bun.sleep(25)
|
||||
stdin.write('d')
|
||||
await Bun.sleep(25)
|
||||
stdin.write('f')
|
||||
await Bun.sleep(25)
|
||||
|
||||
const output = stripAnsi(extractLastFrame(getOutput()))
|
||||
const output = await waitForOutput(
|
||||
getOutput,
|
||||
frame => frame.includes('asdf') && !frame.includes('Type here...'),
|
||||
)
|
||||
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(25)
|
||||
|
||||
expect(output).toContain('asdf')
|
||||
expect(output).not.toContain('Type here...')
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, expect, mock, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
import { createRoot, Text, useTheme } from '../ink.js'
|
||||
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
|
||||
import { AppStateProvider } from '../state/AppState.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import { ThemeProvider } from './design-system/ThemeProvider.js'
|
||||
|
||||
await acquireSharedMutationLock('components/ThemePicker.test.tsx')
|
||||
|
||||
mock.module('./StructuredDiff.js', () => ({
|
||||
StructuredDiff: function StructuredDiffPreview(): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
@@ -115,8 +121,12 @@ async function waitForFrame(
|
||||
return frame
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('updates the preview when keyboard focus moves to another theme', async () => {
|
||||
|
||||
@@ -18,15 +18,21 @@
|
||||
*/
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, expect, mock, test } from 'bun:test'
|
||||
import React, { useEffect } from 'react'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
import { createRoot, Text, useTheme } from '../../ink.js'
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||
import { AppStateProvider } from '../../state/AppState.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { ThemeProvider, usePreviewTheme } from './ThemeProvider.js'
|
||||
|
||||
await acquireSharedMutationLock('components/design-system/ThemeProvider.test.tsx')
|
||||
|
||||
mock.module('../StructuredDiff.js', () => ({
|
||||
StructuredDiff: function StructuredDiffPreview(): React.ReactNode {
|
||||
return <Text>diff</Text>
|
||||
@@ -98,8 +104,12 @@ async function waitForFrame(
|
||||
return frame
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -213,4 +223,4 @@ test('usePreviewTheme() setPreviewTheme changes displayed theme', async () => {
|
||||
stdout.end()
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
|
||||
import { createRoot, Text } from '../ink.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const SYNC_START = '\x1B[?2026h'
|
||||
const SYNC_END = '\x1B[?2026l'
|
||||
@@ -91,8 +95,16 @@ const TOKENS = {
|
||||
apiKey: 'oauth-api-key',
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('components/useCodexOAuthFlow.test.tsx')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('does not persist credentials when downstream setup rejects', async () => {
|
||||
|
||||
@@ -1,29 +1,79 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
|
||||
// MACRO is replaced at build time by Bun.define but not in test mode.
|
||||
// Define it globally so tests that import modules using MACRO don't crash.
|
||||
;(globalThis as Record<string, unknown>).MACRO = {
|
||||
VERSION: '99.0.0',
|
||||
DISPLAY_VERSION: '0.0.0-test',
|
||||
BUILD_TIME: new Date().toISOString(),
|
||||
ISSUES_EXPLAINER: 'report the issue at https://github.com/Gitlawb/openclaude/issues',
|
||||
PACKAGE_URL: '@gitlawb/openclaude',
|
||||
NATIVE_PACKAGE_URL: undefined,
|
||||
}
|
||||
|
||||
import { clearSystemPromptSections } from './systemPromptSections.js'
|
||||
import { getSystemPrompt, DEFAULT_AGENT_PROMPT } from './prompts.js'
|
||||
import { CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } from './system.js'
|
||||
import { CLAUDE_CODE_GUIDE_AGENT } from '../tools/AgentTool/built-in/claudeCodeGuideAgent.js'
|
||||
import { GENERAL_PURPOSE_AGENT } from '../tools/AgentTool/built-in/generalPurposeAgent.js'
|
||||
import { EXPLORE_AGENT } from '../tools/AgentTool/built-in/exploreAgent.js'
|
||||
import { PLAN_AGENT } from '../tools/AgentTool/built-in/planAgent.js'
|
||||
import { STATUSLINE_SETUP_AGENT } from '../tools/AgentTool/built-in/statuslineSetup.js'
|
||||
import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const originalSimpleEnv = process.env.CLAUDE_CODE_SIMPLE
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
const hadOriginalMacro = Object.hasOwn(globalThis, 'MACRO')
|
||||
|
||||
let clearSystemPromptSections: typeof import('./systemPromptSections.js').clearSystemPromptSections
|
||||
let getSystemPrompt: typeof import('./prompts.js').getSystemPrompt
|
||||
let DEFAULT_AGENT_PROMPT: typeof import('./prompts.js').DEFAULT_AGENT_PROMPT
|
||||
let CLI_SYSPROMPT_PREFIXES: typeof import('./system.js').CLI_SYSPROMPT_PREFIXES
|
||||
let getCLISyspromptPrefix: typeof import('./system.js').getCLISyspromptPrefix
|
||||
let CLAUDE_CODE_GUIDE_AGENT:
|
||||
typeof import('../tools/AgentTool/built-in/claudeCodeGuideAgent.js').CLAUDE_CODE_GUIDE_AGENT
|
||||
let GENERAL_PURPOSE_AGENT:
|
||||
typeof import('../tools/AgentTool/built-in/generalPurposeAgent.js').GENERAL_PURPOSE_AGENT
|
||||
let EXPLORE_AGENT:
|
||||
typeof import('../tools/AgentTool/built-in/exploreAgent.js').EXPLORE_AGENT
|
||||
let PLAN_AGENT: typeof import('../tools/AgentTool/built-in/planAgent.js').PLAN_AGENT
|
||||
let STATUSLINE_SETUP_AGENT:
|
||||
typeof import('../tools/AgentTool/built-in/statuslineSetup.js').STATUSLINE_SETUP_AGENT
|
||||
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('constants/promptIdentity.test.ts')
|
||||
|
||||
// MACRO is replaced at build time by Bun.define but not in test mode.
|
||||
// Define it globally under the shared lock before importing modules that use it.
|
||||
;(globalThis as Record<string, unknown>).MACRO = {
|
||||
VERSION: '99.0.0',
|
||||
DISPLAY_VERSION: '0.0.0-test',
|
||||
BUILD_TIME: new Date().toISOString(),
|
||||
ISSUES_EXPLAINER:
|
||||
'report the issue at https://github.com/Gitlawb/openclaude/issues',
|
||||
PACKAGE_URL: '@gitlawb/openclaude',
|
||||
NATIVE_PACKAGE_URL: undefined,
|
||||
}
|
||||
|
||||
;({ clearSystemPromptSections } = await import('./systemPromptSections.js'))
|
||||
;({ getSystemPrompt, DEFAULT_AGENT_PROMPT } = await import('./prompts.js'))
|
||||
;({ CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } = await import('./system.js'))
|
||||
;({ CLAUDE_CODE_GUIDE_AGENT } = await import(
|
||||
'../tools/AgentTool/built-in/claudeCodeGuideAgent.js'
|
||||
))
|
||||
;({ GENERAL_PURPOSE_AGENT } = await import(
|
||||
'../tools/AgentTool/built-in/generalPurposeAgent.js'
|
||||
))
|
||||
;({ EXPLORE_AGENT } = await import(
|
||||
'../tools/AgentTool/built-in/exploreAgent.js'
|
||||
))
|
||||
;({ PLAN_AGENT } = await import('../tools/AgentTool/built-in/planAgent.js'))
|
||||
;({ STATUSLINE_SETUP_AGENT } = await import(
|
||||
'../tools/AgentTool/built-in/statuslineSetup.js'
|
||||
))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
if (hadOriginalMacro) {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
} else {
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimpleEnv
|
||||
if (originalSimpleEnv === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimpleEnv
|
||||
}
|
||||
clearSystemPromptSections()
|
||||
})
|
||||
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
* rather than mocking the tracker module. Fewer moving parts, and the
|
||||
* test fails for the right reason if anyone breaks the wrapping.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { addToTotalSessionCost, resetCostState } from './cost-tracker.js'
|
||||
import {
|
||||
getCurrentTurnCacheMetrics,
|
||||
getSessionCacheMetrics,
|
||||
} from './services/api/cacheStatsTracker.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from './test/sharedMutationLock.js'
|
||||
|
||||
// BetaUsage-compatible shape — minimum fields addToTotalSessionCost
|
||||
// needs to run without throwing. Cache fields are the ones we care
|
||||
@@ -40,12 +44,21 @@ function anthropicUsage(partial: {
|
||||
} as Parameters<typeof addToTotalSessionCost>[1]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('cost-tracker.cacheIntegration.test.ts')
|
||||
// resetCostState is the wrapped version that ALSO clears the cache
|
||||
// tracker — this line is itself part of what we're verifying.
|
||||
resetCostState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
resetCostState()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('addToTotalSessionCost → cacheStatsTracker wiring', () => {
|
||||
test('records normalized cache metrics on the tracker for each call', () => {
|
||||
addToTotalSessionCost(
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { describe, it, expect, mock } from 'bun:test'
|
||||
import { afterAll, describe, it, expect, mock } from 'bun:test'
|
||||
import { getCombinedTools, loadReexposedMcpTools } from './mcp.js'
|
||||
import type { Tool as InternalTool } from '../Tool.js'
|
||||
import type { MCPServerConnection } from '../services/mcp/types.js'
|
||||
import type { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
await acquireSharedMutationLock('entrypoints/mcp.test.ts')
|
||||
|
||||
// Mock the MCP client service to control the tools and connections returned
|
||||
const mockGetMcpToolsCommandsAndResources = mock(async (onConnectionAttempt: any) => {})
|
||||
@@ -10,6 +16,14 @@ mock.module('../services/mcp/client.js', () => ({
|
||||
getMcpToolsCommandsAndResources: mockGetMcpToolsCommandsAndResources
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('getCombinedTools', () => {
|
||||
it('deduplicates builtins when mcpTools have the same name, prioritizing mcpTools', () => {
|
||||
const builtinBash = { name: 'Bash', isMcp: false } as unknown as InternalTool
|
||||
|
||||
@@ -52,9 +52,6 @@ export function assertValidSessionId(sessionId: string): void {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const envMutationQueue: Array<() => void> = []
|
||||
let envMutationLocked = false
|
||||
|
||||
export interface MutexAcquireOptions {
|
||||
/** Maximum time to wait for mutex in milliseconds. Default: no timeout (wait forever). */
|
||||
timeoutMs?: number
|
||||
@@ -67,64 +64,90 @@ export interface MutexAcquireResult {
|
||||
reason?: 'timeout'
|
||||
}
|
||||
|
||||
export async function acquireEnvMutex(options?: MutexAcquireOptions): Promise<MutexAcquireResult> {
|
||||
if (!envMutationLocked) {
|
||||
envMutationLocked = true
|
||||
return { acquired: true }
|
||||
}
|
||||
function createEnvMutexState() {
|
||||
const queue: Array<() => void> = []
|
||||
let locked = false
|
||||
|
||||
if (options?.timeoutMs === undefined) {
|
||||
// No timeout - wait forever (original behavior for backward compatibility)
|
||||
async function acquire(
|
||||
options?: MutexAcquireOptions,
|
||||
): Promise<MutexAcquireResult> {
|
||||
if (!locked) {
|
||||
locked = true
|
||||
return { acquired: true }
|
||||
}
|
||||
|
||||
if (options?.timeoutMs === undefined) {
|
||||
// No timeout - wait forever (original behavior for backward compatibility)
|
||||
return new Promise(resolve => {
|
||||
queue.push(() => resolve({ acquired: true }))
|
||||
})
|
||||
}
|
||||
|
||||
// With timeout - race between queue and timeout
|
||||
return new Promise(resolve => {
|
||||
envMutationQueue.push(() => resolve({ acquired: true }))
|
||||
let resolved = false
|
||||
let callback: () => void
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
// Remove ourselves from the queue to prevent orphaned callback
|
||||
const index = queue.indexOf(callback)
|
||||
if (index !== -1) {
|
||||
queue.splice(index, 1)
|
||||
}
|
||||
resolve({ acquired: false, reason: 'timeout' })
|
||||
}
|
||||
}, options.timeoutMs)
|
||||
|
||||
callback = () => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
clearTimeout(timeoutId)
|
||||
resolve({ acquired: true })
|
||||
}
|
||||
}
|
||||
|
||||
queue.push(callback)
|
||||
})
|
||||
}
|
||||
|
||||
// With timeout - race between queue and timeout
|
||||
return new Promise(resolve => {
|
||||
let resolved = false
|
||||
let callback: () => void
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
// Remove ourselves from the queue to prevent orphaned callback
|
||||
const index = envMutationQueue.indexOf(callback)
|
||||
if (index !== -1) {
|
||||
envMutationQueue.splice(index, 1)
|
||||
function release(): void {
|
||||
if (queue.length > 0) {
|
||||
const next = queue.shift()
|
||||
if (next) {
|
||||
try {
|
||||
next()
|
||||
} catch {
|
||||
// If callback throws, ensure mutex is unlocked so next caller can acquire
|
||||
// The error is intentionally not propagated - callback errors should not
|
||||
// block the mutex system. Callers should handle their own errors.
|
||||
locked = false
|
||||
}
|
||||
resolve({ acquired: false, reason: 'timeout' })
|
||||
}
|
||||
}, options.timeoutMs)
|
||||
|
||||
callback = () => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
clearTimeout(timeoutId)
|
||||
resolve({ acquired: true })
|
||||
}
|
||||
} else {
|
||||
locked = false
|
||||
}
|
||||
}
|
||||
|
||||
envMutationQueue.push(callback)
|
||||
})
|
||||
function reset(): void {
|
||||
queue.length = 0
|
||||
locked = false
|
||||
}
|
||||
|
||||
return { acquire, release, reset }
|
||||
}
|
||||
|
||||
const envMutex = createEnvMutexState()
|
||||
|
||||
export async function acquireEnvMutex(
|
||||
options?: MutexAcquireOptions,
|
||||
): Promise<MutexAcquireResult> {
|
||||
return envMutex.acquire(options)
|
||||
}
|
||||
|
||||
export function releaseEnvMutex(): void {
|
||||
if (envMutationQueue.length > 0) {
|
||||
const next = envMutationQueue.shift()
|
||||
if (next) {
|
||||
try {
|
||||
next()
|
||||
} catch {
|
||||
// If callback throws, ensure mutex is unlocked so next caller can acquire
|
||||
// The error is intentionally not propagated - callback errors should not
|
||||
// block the mutex system. Callers should handle their own errors.
|
||||
envMutationLocked = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
envMutationLocked = false
|
||||
}
|
||||
envMutex.release()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,8 +156,25 @@ export function releaseEnvMutex(): void {
|
||||
* @internal
|
||||
*/
|
||||
export function resetEnvMutexForTesting(): void {
|
||||
envMutationQueue.length = 0
|
||||
envMutationLocked = false
|
||||
envMutex.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an isolated mutex instance for tests that need to exercise timeout
|
||||
* behavior without touching the process-global SDK env mutex.
|
||||
* @internal
|
||||
*/
|
||||
export function createEnvMutexForTesting(): {
|
||||
acquireEnvMutex: typeof acquireEnvMutex
|
||||
releaseEnvMutex: typeof releaseEnvMutex
|
||||
resetEnvMutex: () => void
|
||||
} {
|
||||
const isolated = createEnvMutexState()
|
||||
return {
|
||||
acquireEnvMutex: isolated.acquire,
|
||||
releaseEnvMutex: isolated.release,
|
||||
resetEnvMutex: isolated.reset,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
import { createRoot, Text } from '../ink.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
type AuthState = {
|
||||
anthropicAuthEnabled: boolean
|
||||
@@ -53,8 +57,16 @@ async function waitForCondition(
|
||||
throw new Error('Timed out waiting for useApiKeyVerification test state')
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('hooks/useApiKeyVerification.test.tsx')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('useApiKeyVerification resets stale missing status when the session switches to a third-party provider', async () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalPlatform = process.platform
|
||||
@@ -46,7 +50,8 @@ async function waitForExecCall(
|
||||
}
|
||||
|
||||
describe('Windows clipboard fallback', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('ink/termio/osc.test.ts')
|
||||
installOscMocks()
|
||||
execFileNoThrowMock.mockClear()
|
||||
generateTempFilePathMock.mockClear()
|
||||
@@ -57,8 +62,13 @@ describe('Windows clipboard fallback', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
try {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('uses PowerShell instead of clip.exe for local Windows copy', async () => {
|
||||
@@ -97,7 +107,8 @@ describe('Windows clipboard fallback', () => {
|
||||
})
|
||||
|
||||
describe('clipboard path behavior remains stable', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('ink/termio/osc.test.ts')
|
||||
installOscMocks()
|
||||
execFileNoThrowMock.mockClear()
|
||||
process.env = { ...originalEnv }
|
||||
@@ -106,8 +117,13 @@ describe('clipboard path behavior remains stable', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
try {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('getClipboardPath stays native on local macOS', async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { mkdtempSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { registerGateway } from './index.js'
|
||||
import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from './index.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
@@ -90,6 +90,8 @@ afterEach(() => {
|
||||
restoreEnvValue('CLAUDE_CODE_USE_VERTEX')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_FOUNDRY')
|
||||
restoreEnvValue('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC')
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/integrations/registry.test.ts
|
||||
|
||||
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { ensureIntegrationsLoaded } from './index.js'
|
||||
import {
|
||||
_clearRegistryForTesting,
|
||||
@@ -22,14 +22,23 @@ import {
|
||||
registerVendor,
|
||||
validateIntegrationRegistry,
|
||||
} from './registry.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('integrations/registry.test.ts')
|
||||
_clearRegistryForTesting()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
afterEach(() => {
|
||||
try {
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { beforeEach, expect, test, describe } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test, describe } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import {
|
||||
_setHistoryCapForTesting,
|
||||
getCacheStatsHistory,
|
||||
@@ -21,11 +25,21 @@ function makeMetrics(partial: Partial<CacheMetrics>): CacheMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/api/cacheStatsTracker.test.ts')
|
||||
resetSessionCacheStats()
|
||||
_setHistoryCapForTesting(500)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
resetSessionCacheStats()
|
||||
_setHistoryCapForTesting(500)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('cacheStatsTracker — aggregation', () => {
|
||||
test('currentTurn and session both start empty and unsupported', () => {
|
||||
expect(getCurrentTurnCacheMetrics().supported).toBe(false)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { compressToolHistory, getTiers } from './compressToolHistory.js'
|
||||
|
||||
// Mock the two dependencies so tests are deterministic and don't read disk config.
|
||||
@@ -7,6 +11,8 @@ const mockState = {
|
||||
effectiveWindow: 100_000,
|
||||
}
|
||||
|
||||
await acquireSharedMutationLock('services/api/compressToolHistory.test.ts')
|
||||
|
||||
mock.module('../../utils/config.js', () => ({
|
||||
getGlobalConfig: () => ({
|
||||
toolHistoryCompressionEnabled: mockState.enabled,
|
||||
@@ -22,9 +28,12 @@ beforeEach(() => {
|
||||
mockState.effectiveWindow = 100_000
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockState.enabled = true
|
||||
mockState.effectiveWindow = 100_000
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
type Block = Record<string, unknown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
|
||||
import { createOpenAIShimClient } from './openaiShim.js'
|
||||
|
||||
@@ -17,6 +17,8 @@ const mockState = {
|
||||
effectiveWindow: 100_000, // Copilot gpt-4o tier
|
||||
}
|
||||
|
||||
await acquireSharedMutationLock('openaiShim.compression.test.ts')
|
||||
|
||||
mock.module('../../utils/config.js', () => ({
|
||||
getGlobalConfig: () => ({
|
||||
toolHistoryCompressionEnabled: mockState.enabled,
|
||||
@@ -97,7 +99,6 @@ function makeFakeResponse(): Response {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('openaiShim.compression.test.ts')
|
||||
process.env.OPENAI_BASE_URL = 'http://example.test/v1'
|
||||
process.env.OPENAI_API_KEY = 'test-key'
|
||||
delete process.env.OPENAI_MODEL
|
||||
@@ -106,14 +107,18 @@ beforeEach(async () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv.OPENAI_BASE_URL === undefined) delete process.env.OPENAI_BASE_URL
|
||||
else process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
|
||||
if (originalEnv.OPENAI_API_KEY === undefined) delete process.env.OPENAI_API_KEY
|
||||
else process.env.OPENAI_API_KEY = originalEnv.OPENAI_API_KEY
|
||||
if (originalEnv.OPENAI_MODEL === undefined) delete process.env.OPENAI_MODEL
|
||||
else process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
if (originalEnv.OPENAI_BASE_URL === undefined) delete process.env.OPENAI_BASE_URL
|
||||
else process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
|
||||
if (originalEnv.OPENAI_API_KEY === undefined) delete process.env.OPENAI_API_KEY
|
||||
else process.env.OPENAI_API_KEY = originalEnv.OPENAI_API_KEY
|
||||
if (originalEnv.OPENAI_MODEL === undefined) delete process.env.OPENAI_MODEL
|
||||
else process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
|
||||
globalThis.fetch = originalFetch
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
|
||||
import { registerGateway } from '../../integrations/index.ts'
|
||||
import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from '../../integrations/index.ts'
|
||||
import { createOpenAIShimClient } from './openaiShim.ts'
|
||||
|
||||
type FetchType = typeof globalThis.fetch
|
||||
@@ -151,6 +151,8 @@ afterEach(() => {
|
||||
restoreEnv('DEEPSEEK_API_KEY', originalEnv.DEEPSEEK_API_KEY)
|
||||
restoreEnv('MIMO_API_KEY', originalEnv.MIMO_API_KEY)
|
||||
globalThis.fetch = originalFetch
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { resolveRuntimeCodexCredentials } from './providerConfig.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/api/providerConfig.runtimeCodexCredentials.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
function makeJwt(payload: Record<string, unknown>): string {
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
getEffectiveContextWindowSize,
|
||||
getAutoCompactThreshold,
|
||||
} from './autoCompact.ts'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
let originalUseOpenAI: string | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/compact/autoCompact.test.ts')
|
||||
originalUseOpenAI = process.env.CLAUDE_CODE_USE_OPENAI
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (originalUseOpenAI === undefined) {
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = originalUseOpenAI
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('getEffectiveContextWindowSize', () => {
|
||||
test('returns positive value for known models with large context windows', () => {
|
||||
@@ -24,16 +47,12 @@ describe('getEffectiveContextWindowSize', () => {
|
||||
// disabled it's 20k + 13k = 33k. Assert the worst case so the test is
|
||||
// stable regardless of flag state in CI vs local.
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
try {
|
||||
const effective = getEffectiveContextWindowSize('some-unknown-3p-model')
|
||||
expect(effective).toBeGreaterThan(0)
|
||||
// 21k = CAPPED_DEFAULT_MAX_TOKENS (8k) + AUTOCOMPACT_BUFFER_TOKENS (13k).
|
||||
// Covers the anti-regression intent of issue #635 without assuming
|
||||
// the GrowthBook flag state.
|
||||
expect(effective).toBeGreaterThanOrEqual(21_000)
|
||||
} finally {
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
}
|
||||
const effective = getEffectiveContextWindowSize('some-unknown-3p-model')
|
||||
expect(effective).toBeGreaterThan(0)
|
||||
// 21k = CAPPED_DEFAULT_MAX_TOKENS (8k) + AUTOCOMPACT_BUFFER_TOKENS (13k).
|
||||
// Covers the anti-regression intent of issue #635 without assuming
|
||||
// the GrowthBook flag state.
|
||||
expect(effective).toBeGreaterThanOrEqual(21_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,11 +64,7 @@ describe('getAutoCompactThreshold', () => {
|
||||
|
||||
test('never returns negative threshold even for unknown 3P models (issue #635)', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
try {
|
||||
const threshold = getAutoCompactThreshold('some-unknown-3p-model')
|
||||
expect(threshold).toBeGreaterThan(0)
|
||||
} finally {
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
}
|
||||
const threshold = getAutoCompactThreshold('some-unknown-3p-model')
|
||||
expect(threshold).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
import {
|
||||
DEFAULT_GITHUB_DEVICE_SCOPE,
|
||||
@@ -12,8 +16,16 @@ async function importFreshModule() {
|
||||
return import(`./deviceFlow.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/github/deviceFlow.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('requestDeviceCode', () => {
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalAxiosGet = axios.get
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
return import(`./officialRegistry.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/mcp/officialRegistry.test.ts')
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
mock.restore()
|
||||
try {
|
||||
process.env = { ...originalEnv }
|
||||
axios.get = originalAxiosGet
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('prefetchOfficialMcpUrls', () => {
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('services/oauth/auth-code-listener.analytics.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('custom error responses log the error redirect analytics event', async () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
type StubSettings = {
|
||||
sponsoredTipsEnabled?: boolean
|
||||
@@ -12,6 +16,8 @@ const configRef: {
|
||||
} = { value: { numStartups: 100 } }
|
||||
|
||||
// mock.module is process-global — install once, then mutate the refs per test.
|
||||
await acquireSharedMutationLock('services/tips/sponsoredTips.test.ts')
|
||||
|
||||
mock.module('../../utils/settings/settings.js', () => ({
|
||||
getSettings_DEPRECATED: () => settingsRef.value,
|
||||
getInitialSettings: () => settingsRef.value,
|
||||
@@ -25,6 +31,14 @@ mock.module('../../utils/config.js', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function freshImport() {
|
||||
const stamp = `${Date.now()}-${Math.random()}`
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import type { Tip } from './types.js'
|
||||
|
||||
const settingsRef: {
|
||||
@@ -18,6 +22,8 @@ const configRef: {
|
||||
|
||||
const relevantTipsRef: { value: Tip[] } = { value: [] }
|
||||
|
||||
await acquireSharedMutationLock('services/tips/tipScheduler.test.ts')
|
||||
|
||||
mock.module('../../utils/settings/settings.js', () => ({
|
||||
getSettings_DEPRECATED: () => settingsRef.value,
|
||||
getInitialSettings: () => settingsRef.value,
|
||||
@@ -39,6 +45,14 @@ mock.module('../analytics/index.js', () => ({
|
||||
logEvent: () => undefined,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function freshScheduler() {
|
||||
const stamp = `${Date.now()}-${Math.random()}`
|
||||
return import(`./tipScheduler.ts?ts=${stamp}`)
|
||||
|
||||
+50
-1
@@ -1,18 +1,67 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { getEmptyToolPermissionContext } from './Tool.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from './test/sharedMutationLock.js'
|
||||
|
||||
let lspConnected = false
|
||||
const HOOK_EVENTS = [
|
||||
'PreToolUse',
|
||||
'PostToolUse',
|
||||
'PostToolUseFailure',
|
||||
'Notification',
|
||||
'UserPromptSubmit',
|
||||
'SessionStart',
|
||||
'SessionEnd',
|
||||
'Stop',
|
||||
'StopFailure',
|
||||
'SubagentStart',
|
||||
'SubagentStop',
|
||||
'PreCompact',
|
||||
'PostCompact',
|
||||
'PermissionRequest',
|
||||
'PermissionDenied',
|
||||
'Setup',
|
||||
'TeammateIdle',
|
||||
'TaskCreated',
|
||||
'TaskCompleted',
|
||||
'Elicitation',
|
||||
'ElicitationResult',
|
||||
'ConfigChange',
|
||||
'WorktreeCreate',
|
||||
'WorktreeRemove',
|
||||
'InstructionsLoaded',
|
||||
'CwdChanged',
|
||||
'FileChanged',
|
||||
] as const
|
||||
|
||||
await acquireSharedMutationLock('tools.lsp.test.ts')
|
||||
|
||||
mock.module('./entrypoints/agentSdkTypes.js', () => ({ HOOK_EVENTS }))
|
||||
mock.module('src/entrypoints/agentSdkTypes.js', () => ({ HOOK_EVENTS }))
|
||||
|
||||
mock.module('./services/lsp/manager.js', () => ({
|
||||
getInitializationStatus: () => ({ status: 'success' }),
|
||||
getLspServerManager: () => undefined,
|
||||
initializeLspServerManager: async () => {},
|
||||
isLspConnected: () => lspConnected,
|
||||
reinitializeLspServerManager: () => {},
|
||||
resetLspServerManagerForTesting: () => {},
|
||||
shutdownLspServerManager: async () => {},
|
||||
waitForInitialization: async () => {},
|
||||
}))
|
||||
|
||||
const { getAllBaseTools, getTools } = await import('./tools.js')
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
lspConnected = false
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { bashToolHasPermission, stripAllLeadingEnvVars } from './bashPermissions.js'
|
||||
|
||||
@@ -11,15 +15,30 @@ const originalSandboxMethods = {
|
||||
areUnsandboxedCommandsAllowed: SandboxManager.areUnsandboxedCommandsAllowed,
|
||||
getExcludedCommands: SandboxManager.getExcludedCommands,
|
||||
}
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
const hadOriginalMacro = Object.hasOwn(globalThis, 'MACRO')
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('tools/BashTool/bashPermissions.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
SandboxManager.isSandboxingEnabled =
|
||||
originalSandboxMethods.isSandboxingEnabled
|
||||
SandboxManager.isAutoAllowBashIfSandboxedEnabled =
|
||||
originalSandboxMethods.isAutoAllowBashIfSandboxedEnabled
|
||||
SandboxManager.areUnsandboxedCommandsAllowed =
|
||||
originalSandboxMethods.areUnsandboxedCommandsAllowed
|
||||
SandboxManager.getExcludedCommands = originalSandboxMethods.getExcludedCommands
|
||||
try {
|
||||
SandboxManager.isSandboxingEnabled =
|
||||
originalSandboxMethods.isSandboxingEnabled
|
||||
SandboxManager.isAutoAllowBashIfSandboxedEnabled =
|
||||
originalSandboxMethods.isAutoAllowBashIfSandboxedEnabled
|
||||
SandboxManager.areUnsandboxedCommandsAllowed =
|
||||
originalSandboxMethods.areUnsandboxedCommandsAllowed
|
||||
SandboxManager.getExcludedCommands = originalSandboxMethods.getExcludedCommands
|
||||
if (hadOriginalMacro) {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
} else {
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
function makeToolUseContext() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { BashTool } from './BashTool.js'
|
||||
import { PowerShellTool } from '../PowerShellTool/PowerShellTool.js'
|
||||
@@ -10,11 +14,19 @@ const originalSandboxMethods = {
|
||||
areUnsandboxedCommandsAllowed: SandboxManager.areUnsandboxedCommandsAllowed,
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('tools/BashTool/shouldUseSandbox.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
SandboxManager.isSandboxingEnabled =
|
||||
originalSandboxMethods.isSandboxingEnabled
|
||||
SandboxManager.areUnsandboxedCommandsAllowed =
|
||||
originalSandboxMethods.areUnsandboxedCommandsAllowed
|
||||
try {
|
||||
SandboxManager.isSandboxingEnabled =
|
||||
originalSandboxMethods.isSandboxingEnabled
|
||||
SandboxManager.areUnsandboxedCommandsAllowed =
|
||||
originalSandboxMethods.areUnsandboxedCommandsAllowed
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('model-facing Bash schema rejects dangerouslyDisableSandbox', () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
// Mock the Anthropic-API-side before importing the module under test, so
|
||||
// queryHaiku resolves into whatever the individual test wants (slow, failing,
|
||||
@@ -7,6 +11,7 @@ import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
const haikuMock = mock()
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('tools/WebFetchTool/applyPromptFallback.test.ts')
|
||||
haikuMock.mockReset()
|
||||
const actual = await import('../../services/api/claude.js')
|
||||
mock.module('../../services/api/claude.js', () => ({
|
||||
@@ -16,7 +21,11 @@ beforeEach(async () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function runApply(markdown = 'Hello world.', signal?: AbortSignal): Promise<string> {
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalAxiosGet = axios.get
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
return import(`./utils.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('tools/WebFetchTool/domainCheck.test.ts')
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
mock.restore()
|
||||
try {
|
||||
process.env = { ...originalEnv }
|
||||
axios.get = originalAxiosGet
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('checkDomainBlocklist', () => {
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { braveProvider } from './brave.ts'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../../test/sharedMutationLock.js'
|
||||
|
||||
import { braveProvider } from './brave.ts'
|
||||
|
||||
const originalEnv = {
|
||||
BRAVE_API_KEY: process.env.BRAVE_API_KEY,
|
||||
}
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
for (const [k, v] of Object.entries(originalEnv)) {
|
||||
if (v === undefined) delete process.env[k]
|
||||
else process.env[k] = v
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('WebSearchTool/providers/brave.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
for (const [k, v] of Object.entries(originalEnv)) {
|
||||
if (v === undefined) delete process.env[k]
|
||||
else process.env[k] = v
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('braveProvider isConfigured', () => {
|
||||
test('true when BRAVE_API_KEY is set', () => {
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../../test/sharedMutationLock.js'
|
||||
import { extractHits, customProvider, isPrivateHostname } from './custom.js'
|
||||
|
||||
async function importFreshCustomProvider() {
|
||||
const stamp = `${Date.now()}-${Math.random()}`
|
||||
return import(`./custom.ts?ts=${stamp}`)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('WebSearchTool/providers/custom.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
releaseSharedMutationLock()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extractHits — flexible response parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -261,8 +278,8 @@ describe('built-in preset request shapes', () => {
|
||||
return new Response(JSON.stringify({ items: [] }), { status: 200 })
|
||||
}) as typeof fetch
|
||||
|
||||
const { customProvider } = require('./custom.js')
|
||||
await customProvider.search({ query: 'hello world' })
|
||||
const { customProvider: freshCustomProvider } = await importFreshCustomProvider()
|
||||
await freshCustomProvider.search({ query: 'hello world' })
|
||||
|
||||
expect(capturedUrl).toContain('https://www.googleapis.com/customsearch/v1')
|
||||
expect(capturedUrl).toContain('key=gck-test-key')
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { exaProvider } from './exa.ts'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../../test/sharedMutationLock.js'
|
||||
|
||||
import { exaProvider } from './exa.ts'
|
||||
|
||||
const originalEnv = {
|
||||
EXA_API_KEY: process.env.EXA_API_KEY,
|
||||
}
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
for (const [k, v] of Object.entries(originalEnv)) {
|
||||
if (v === undefined) delete process.env[k]
|
||||
else process.env[k] = v
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('WebSearchTool/providers/exa.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
for (const [k, v] of Object.entries(originalEnv)) {
|
||||
if (v === undefined) delete process.env[k]
|
||||
else process.env[k] = v
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('exaProvider isConfigured', () => {
|
||||
test('true when EXA_API_KEY is set', () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../../test/sharedMutationLock.js'
|
||||
|
||||
import { firecrawlProvider } from './firecrawl.ts'
|
||||
|
||||
@@ -15,9 +19,17 @@ function restoreEnv(key: string, value: string | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('WebSearchTool/providers/firecrawl.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv('FIRECRAWL_API_KEY', originalEnv.FIRECRAWL_API_KEY)
|
||||
restoreEnv('FIRECRAWL_API_URL', originalEnv.FIRECRAWL_API_URL)
|
||||
try {
|
||||
restoreEnv('FIRECRAWL_API_KEY', originalEnv.FIRECRAWL_API_KEY)
|
||||
restoreEnv('FIRECRAWL_API_URL', originalEnv.FIRECRAWL_API_URL)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('firecrawlProvider isConfigured', () => {
|
||||
@@ -28,7 +40,7 @@ describe('firecrawlProvider isConfigured', () => {
|
||||
})
|
||||
|
||||
test('true when FIRECRAWL_API_URL is set only', () => {
|
||||
process.env.FIRECRAWL_API_KEY = undefined
|
||||
delete process.env.FIRECRAWL_API_KEY
|
||||
process.env.FIRECRAWL_API_URL = 'https://self-hosted.firecrawl.dev'
|
||||
expect(firecrawlProvider.isConfigured()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../../test/sharedMutationLock.js'
|
||||
import { getProviderMode, getProviderChain, getAvailableProviders } from './index.js'
|
||||
import type { ProviderMode } from './index.js'
|
||||
|
||||
const savedWebSearchEnv = {
|
||||
WEB_SEARCH_PROVIDER: process.env.WEB_SEARCH_PROVIDER,
|
||||
TAVILY_API_KEY: process.env.TAVILY_API_KEY,
|
||||
}
|
||||
|
||||
function restoreWebSearchEnv() {
|
||||
for (const [key, value] of Object.entries(savedWebSearchEnv)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('WebSearchTool/providers/index.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
restoreWebSearchEnv()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getProviderMode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('getProviderMode', () => {
|
||||
const savedEnv = process.env.WEB_SEARCH_PROVIDER
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv === undefined) {
|
||||
delete process.env.WEB_SEARCH_PROVIDER
|
||||
} else {
|
||||
process.env.WEB_SEARCH_PROVIDER = savedEnv
|
||||
}
|
||||
})
|
||||
|
||||
test('returns auto by default', () => {
|
||||
delete process.env.WEB_SEARCH_PROVIDER
|
||||
expect(getProviderMode()).toBe('auto')
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realProviders from './model/providers.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalFetch = globalThis.fetch
|
||||
@@ -8,20 +13,27 @@ async function importFreshModule() {
|
||||
return import(`./apiPreconnect.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/apiPreconnect.test.ts')
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
globalThis.fetch = originalFetch
|
||||
mock.restore()
|
||||
try {
|
||||
process.env = { ...originalEnv }
|
||||
globalThis.fetch = originalFetch
|
||||
mock.restore()
|
||||
mock.module('./model/providers.js', () => realProviders)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('preconnectAnthropicApi', () => {
|
||||
test('does not fetch when OpenAI mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'openai',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
@@ -36,6 +48,7 @@ describe('preconnectAnthropicApi', () => {
|
||||
test('does not fetch when Gemini mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'gemini',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
@@ -50,6 +63,7 @@ describe('preconnectAnthropicApi', () => {
|
||||
test('does not fetch when GitHub mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'github',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
@@ -77,6 +91,7 @@ describe('preconnectAnthropicApi', () => {
|
||||
delete process.env.CLAUDE_CODE_CLIENT_KEY
|
||||
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'firstParty',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
import { isAntEmployee } from './buildConfig.ts'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
// Finding #42-2: process.env.USER_TYPE === 'ant' is checked directly in multiple
|
||||
// places, allowing any external user to activate Anthropic-internal code paths.
|
||||
// In OpenClaude, this must always be false regardless of env var.
|
||||
|
||||
let originalUserType: string | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/buildConfig.test.ts')
|
||||
originalUserType = process.env.USER_TYPE
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (originalUserType === undefined) {
|
||||
delete process.env.USER_TYPE
|
||||
} else {
|
||||
process.env.USER_TYPE = originalUserType
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('isAntEmployee always returns false in OpenClaude regardless of USER_TYPE env var', () => {
|
||||
const original = process.env.USER_TYPE
|
||||
process.env.USER_TYPE = 'ant'
|
||||
expect(isAntEmployee()).toBe(false)
|
||||
process.env.USER_TYPE = original
|
||||
})
|
||||
|
||||
test('isAntEmployee returns false even when USER_TYPE is unset', () => {
|
||||
const original = process.env.USER_TYPE
|
||||
delete process.env.USER_TYPE
|
||||
expect(isAntEmployee()).toBe(false)
|
||||
process.env.USER_TYPE = original
|
||||
})
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
* These tests avoid static imports so Bun can mock secureStorage before
|
||||
* codexCredentials is first loaded.
|
||||
*/
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
function makeJwt(payload: Record<string, unknown>): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' }))
|
||||
@@ -16,20 +20,28 @@ describe('codexCredentials', () => {
|
||||
const originalCodeKey = process.env.CODEX_API_KEY
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/codexCredentials.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
globalThis.fetch = originalFetch
|
||||
try {
|
||||
mock.restore()
|
||||
globalThis.fetch = originalFetch
|
||||
|
||||
if (originalSimple === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimple
|
||||
}
|
||||
if (originalSimple === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimple
|
||||
}
|
||||
|
||||
if (originalCodeKey === undefined) {
|
||||
delete process.env.CODEX_API_KEY
|
||||
} else {
|
||||
process.env.CODEX_API_KEY = originalCodeKey
|
||||
if (originalCodeKey === undefined) {
|
||||
delete process.env.CODEX_API_KEY
|
||||
} else {
|
||||
process.env.CODEX_API_KEY = originalCodeKey
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
* conversationRecovery so Bun's mock.module can replace sessionStart before
|
||||
* that module is first loaded.
|
||||
*/
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realProviders from './model/providers.js'
|
||||
import * as realSessionStart from './sessionStart.js'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
const originalEnv = { ...process.env }
|
||||
@@ -44,13 +50,24 @@ async function writeJsonl(entry: unknown): Promise<string> {
|
||||
return filePath
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/conversationRecovery.hooks.test.ts')
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'firstParty',
|
||||
}))
|
||||
process.env = { ...originalEnv }
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
mock.restore()
|
||||
mock.module('./model/providers.js', () => realProviders)
|
||||
mock.module('./sessionStart.js', () => realSessionStart)
|
||||
process.env = { ...originalEnv }
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('loadConversationForResume rejects oversized transcripts before resume hooks run', async () => {
|
||||
@@ -60,6 +77,7 @@ test('loadConversationForResume rejects oversized transcripts before resume hook
|
||||
const hookSpy = mock(() => Promise.resolve([{ type: 'hook' }]))
|
||||
|
||||
mock.module('./sessionStart.js', () => ({
|
||||
...realSessionStart,
|
||||
processSessionStartHooks: hookSpy,
|
||||
}))
|
||||
|
||||
@@ -109,6 +127,7 @@ test('deserializeMessagesWithInterruptDetection strips thinking blocks only for
|
||||
]
|
||||
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'openai',
|
||||
}))
|
||||
|
||||
@@ -130,6 +149,7 @@ test('deserializeMessagesWithInterruptDetection strips thinking blocks only for
|
||||
).not.toContain('only hidden reasoning')
|
||||
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => 'bedrock',
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realProviders from './model/providers.js'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
const originalSimple = process.env.CLAUDE_CODE_SIMPLE
|
||||
@@ -59,23 +64,37 @@ async function writeJsonl(entry: unknown): Promise<string> {
|
||||
return filePath
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/conversationRecovery.test.ts')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimple
|
||||
for (const key of providerEnvKeys) {
|
||||
const value = originalProviderEnv[key]
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
try {
|
||||
mock.restore()
|
||||
mock.module('./model/providers.js', () => realProviders)
|
||||
if (originalSimple === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env[key] = value
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimple
|
||||
}
|
||||
for (const key of providerEnvKeys) {
|
||||
const value = originalProviderEnv[key]
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
async function importFreshConversationRecovery() {
|
||||
mock.restore()
|
||||
mock.module('./model/providers.js', () => ({
|
||||
...realProviders,
|
||||
getAPIProvider: () => {
|
||||
if (process.env.CLAUDE_CODE_USE_GITHUB) return 'github'
|
||||
if (process.env.CLAUDE_CODE_USE_OPENAI) return 'openai'
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
// Import the real auth.js and providerConfig.js up front so we can spread
|
||||
// their export surfaces into mock factories. `mock.module()` is process-global
|
||||
// in bun:test and `mock.restore()` does not undo it (see user.test.ts), so
|
||||
@@ -12,8 +16,16 @@ import * as actualGrowthbook from 'src/services/analytics/growthbook.js'
|
||||
import * as actualProviders from './model/providers.js'
|
||||
import * as actualModelSupportOverrides from './model/modelSupportOverrides.js'
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/effort.codex.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function importFreshEffortModule(options: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
@@ -55,11 +55,15 @@ test('execFileNoThrowWithCwd preserves Windows .cmd compatibility', async () =>
|
||||
const { execFileNoThrowWithCwd } = await importFreshExecFileNoThrowModule()
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), 'openclaude-execfile-'))
|
||||
const file = join(dir, 'hello.cmd')
|
||||
writeFileSync(file, '@echo off\r\necho hello\r\n')
|
||||
try {
|
||||
const file = join(dir, 'hello.cmd')
|
||||
writeFileSync(file, '@echo off\r\necho hello\r\n')
|
||||
|
||||
const result = await execFileNoThrowWithCwd(file, [])
|
||||
const result = await execFileNoThrowWithCwd(file, [])
|
||||
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout).toContain('hello')
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout).toContain('hello')
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
@@ -188,13 +192,21 @@ function forceFirstPartyProviderEnv(): void {
|
||||
delete process.env.OPENAI_MODEL
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/fastMode.test.ts')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
const { resetStateForTests } = await import('../bootstrap/state.js')
|
||||
resetStateForTests()
|
||||
const { _setGlobalConfigCacheForTesting } = await import('./config.js')
|
||||
_setGlobalConfigCacheForTesting(null)
|
||||
try {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
const { resetStateForTests } = await import('../bootstrap/state.js')
|
||||
resetStateForTests()
|
||||
const { _setGlobalConfigCacheForTesting } = await import('./config.js')
|
||||
_setGlobalConfigCacheForTesting(null)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('fastMode ant-only fallback cleanup', () => {
|
||||
|
||||
+15
-3
@@ -1,4 +1,8 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
async function importFileModuleWithKillswitchEnabled(
|
||||
killswitchEnabled: boolean,
|
||||
@@ -10,8 +14,16 @@ async function importFileModuleWithKillswitchEnabled(
|
||||
return import(`./file.js?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('utils/file.test.ts')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('addLineNumbers', () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import {
|
||||
getGeminiProjectIdHint,
|
||||
mayHaveGeminiAdcCredentials,
|
||||
@@ -28,19 +32,27 @@ function restoreEnv(key: string, value: string | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/geminiAuth.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
|
||||
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
|
||||
restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN)
|
||||
restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE)
|
||||
restoreEnv(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
originalEnv.GOOGLE_APPLICATION_CREDENTIALS,
|
||||
)
|
||||
restoreEnv('GOOGLE_CLOUD_PROJECT', originalEnv.GOOGLE_CLOUD_PROJECT)
|
||||
restoreEnv('GCLOUD_PROJECT', originalEnv.GCLOUD_PROJECT)
|
||||
restoreEnv('GOOGLE_PROJECT_ID', originalEnv.GOOGLE_PROJECT_ID)
|
||||
restoreEnv('APPDATA', originalEnv.APPDATA)
|
||||
try {
|
||||
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
|
||||
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
|
||||
restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN)
|
||||
restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE)
|
||||
restoreEnv(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
originalEnv.GOOGLE_APPLICATION_CREDENTIALS,
|
||||
)
|
||||
restoreEnv('GOOGLE_CLOUD_PROJECT', originalEnv.GOOGLE_CLOUD_PROJECT)
|
||||
restoreEnv('GCLOUD_PROJECT', originalEnv.GCLOUD_PROJECT)
|
||||
restoreEnv('GOOGLE_PROJECT_ID', originalEnv.GOOGLE_PROJECT_ID)
|
||||
restoreEnv('APPDATA', originalEnv.APPDATA)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('resolveGeminiCredential', () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
type MockStorageData = Record<string, unknown>
|
||||
|
||||
@@ -26,7 +30,8 @@ async function importFreshModule() {
|
||||
return import(`./geminiCredentials.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/geminiCredentials.test.ts')
|
||||
process.env = { ...originalEnv }
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
process.argv = originalArgv.filter(arg => arg !== '--bare')
|
||||
@@ -34,10 +39,14 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
process.argv = [...originalArgv]
|
||||
storageState = {}
|
||||
mock.restore()
|
||||
try {
|
||||
process.env = { ...originalEnv }
|
||||
process.argv = [...originalArgv]
|
||||
storageState = {}
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('saveGeminiAccessToken stores and reads back the token', async () => {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
* githubModelsCredentials so Bun's mock.module can replace secureStorage
|
||||
* before that module is first loaded.
|
||||
*/
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
describe('hydrateGithubModelsTokenFromSecureStorage', () => {
|
||||
const orig = {
|
||||
@@ -15,14 +19,22 @@ describe('hydrateGithubModelsTokenFromSecureStorage', () => {
|
||||
CLAUDE_CODE_SIMPLE: process.env.CLAUDE_CODE_SIMPLE,
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/githubModelsCredentials.hydrate.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k as keyof typeof orig]
|
||||
} else {
|
||||
process.env[k as keyof typeof orig] = v
|
||||
try {
|
||||
mock.restore()
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k as keyof typeof orig]
|
||||
} else {
|
||||
process.env[k as keyof typeof orig] = v
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realDeviceFlow from '../services/github/deviceFlow.js'
|
||||
import * as realSecureStorage from './secureStorage/index.js'
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
@@ -13,17 +19,25 @@ describe('refreshGithubModelsTokenIfNeeded', () => {
|
||||
GH_TOKEN: process.env.GH_TOKEN,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/githubModelsCredentials.refresh.test.ts')
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k as keyof typeof orig]
|
||||
} else {
|
||||
process.env[k as keyof typeof orig] = v
|
||||
try {
|
||||
mock.restore()
|
||||
mock.module('./secureStorage/index.js', () => realSecureStorage)
|
||||
mock.module('../services/github/deviceFlow.js', () => realDeviceFlow)
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k as keyof typeof orig]
|
||||
} else {
|
||||
process.env[k as keyof typeof orig] = v
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,6 +56,7 @@ describe('refreshGithubModelsTokenIfNeeded', () => {
|
||||
}
|
||||
|
||||
mock.module('./secureStorage/index.js', () => ({
|
||||
...realSecureStorage,
|
||||
getSecureStorage: () => ({
|
||||
read: () => store,
|
||||
update: (next: Record<string, unknown>) => {
|
||||
@@ -52,6 +67,7 @@ describe('refreshGithubModelsTokenIfNeeded', () => {
|
||||
}))
|
||||
|
||||
mock.module('../services/github/deviceFlow.js', () => ({
|
||||
...realDeviceFlow,
|
||||
DEFAULT_GITHUB_DEVICE_SCOPE: 'read:user',
|
||||
exchangeForCopilotToken: async () => ({
|
||||
token: `tid=fresh;exp=${futureExp};sku=free`,
|
||||
@@ -90,6 +106,7 @@ describe('refreshGithubModelsTokenIfNeeded', () => {
|
||||
}))
|
||||
|
||||
mock.module('./secureStorage/index.js', () => ({
|
||||
...realSecureStorage,
|
||||
getSecureStorage: () => ({
|
||||
read: () => ({
|
||||
githubModels: {
|
||||
@@ -102,6 +119,7 @@ describe('refreshGithubModelsTokenIfNeeded', () => {
|
||||
}))
|
||||
|
||||
mock.module('../services/github/deviceFlow.js', () => ({
|
||||
...realDeviceFlow,
|
||||
DEFAULT_GITHUB_DEVICE_SCOPE: 'read:user',
|
||||
exchangeForCopilotToken: exchangeSpy,
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const originalSimple = process.env.CLAUDE_CODE_SIMPLE
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/githubModelsCredentials.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (originalSimple === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = originalSimple
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('readGithubModelsToken', () => {
|
||||
test('returns undefined in bare mode', async () => {
|
||||
@@ -6,14 +28,8 @@ describe('readGithubModelsToken', () => {
|
||||
'./githubModelsCredentials.js?read-bare-mode'
|
||||
)
|
||||
|
||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||
expect(readGithubModelsToken()).toBeUndefined()
|
||||
if (prev === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = prev
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,16 +39,10 @@ describe('saveGithubModelsToken / clearGithubModelsToken', () => {
|
||||
'./githubModelsCredentials.js?save-bare-mode'
|
||||
)
|
||||
|
||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||
const r = saveGithubModelsToken('abc')
|
||||
expect(r.success).toBe(false)
|
||||
expect(r.warning).toContain('Bare mode')
|
||||
if (prev === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = prev
|
||||
}
|
||||
})
|
||||
|
||||
test('clear succeeds in bare mode', async () => {
|
||||
@@ -40,14 +50,8 @@ describe('saveGithubModelsToken / clearGithubModelsToken', () => {
|
||||
'./githubModelsCredentials.js?clear-bare-mode'
|
||||
)
|
||||
|
||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||
expect(clearGithubModelsToken().success).toBe(true)
|
||||
if (prev === undefined) {
|
||||
delete process.env.CLAUDE_CODE_SIMPLE
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SIMPLE = prev
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import { getCommandQueue, resetCommandQueue } from './messageQueueManager.js'
|
||||
|
||||
describe('handlePromptSubmit', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/handlePromptSubmit.test.ts')
|
||||
resetCommandQueue()
|
||||
mock.module('src/services/analytics/index.js', () => ({
|
||||
logEvent: () => {},
|
||||
@@ -10,8 +15,12 @@ describe('handlePromptSubmit', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetCommandQueue()
|
||||
mock.restore()
|
||||
try {
|
||||
resetCommandQueue()
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
it('queues prompt submissions during generation without interrupting the current turn', async () => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
type HookChainsModule = typeof import('./hookChains.js')
|
||||
|
||||
@@ -100,22 +104,27 @@ async function importHookChainsHarness(
|
||||
return { mod, writeToMailboxSpy, agentToolCallSpy }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/hookChains.integration.test.ts')
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = '1'
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
|
||||
if (originalHookChainsEnabled === undefined) {
|
||||
delete process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = originalHookChainsEnabled
|
||||
if (originalHookChainsEnabled === undefined) {
|
||||
delete process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = originalHookChainsEnabled
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })),
|
||||
)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })),
|
||||
)
|
||||
})
|
||||
|
||||
describe('hookChains integration dispatch', () => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
type HookChainsModule = typeof import('./hookChains.js')
|
||||
|
||||
@@ -38,22 +42,27 @@ async function importHookChainsModule(options?: {
|
||||
return import(`./hookChains.js?test=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/hookChains.test.ts')
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = '1'
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
|
||||
if (originalHookChainsEnabled === undefined) {
|
||||
delete process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = originalHookChainsEnabled
|
||||
if (originalHookChainsEnabled === undefined) {
|
||||
delete process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_ENABLE_HOOK_CHAINS = originalHookChainsEnabled
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })),
|
||||
)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })),
|
||||
)
|
||||
})
|
||||
|
||||
describe('hookChains schema validation', () => {
|
||||
|
||||
@@ -111,9 +111,14 @@ function getCacheAge(message: Message): number {
|
||||
}
|
||||
|
||||
function getMessageTokenCount(message: Message): number {
|
||||
const countTextTokens = (text: string): number => {
|
||||
if (text.length === 0) return 0
|
||||
return Math.max(1, roughTokenCountEstimation(text))
|
||||
}
|
||||
|
||||
const content = message.message?.content
|
||||
if (typeof content === 'string') {
|
||||
return roughTokenCountEstimation(content)
|
||||
return countTextTokens(content)
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
let tokens = 0
|
||||
@@ -123,17 +128,17 @@ function getMessageTokenCount(message: Message): number {
|
||||
const b = block as Record<string, unknown>
|
||||
|
||||
if (b.type === 'text' && typeof b.text === 'string') {
|
||||
tokens += roughTokenCountEstimation(b.text)
|
||||
tokens += countTextTokens(b.text)
|
||||
} else if (b.type === 'tool_use') {
|
||||
const inputSize = JSON.stringify(b.input ?? {}).length
|
||||
tokens += Math.ceil(inputSize / 4) + 20
|
||||
} else if (b.type === 'tool_result') {
|
||||
if (typeof b.content === 'string') {
|
||||
tokens += roughTokenCountEstimation(b.content)
|
||||
tokens += countTextTokens(b.content)
|
||||
} else if (Array.isArray(b.content)) {
|
||||
for (const rc of b.content) {
|
||||
if (typeof rc === 'object' && rc !== null && 'text' in rc) {
|
||||
tokens += roughTokenCountEstimation((rc as { text: string }).text)
|
||||
tokens += countTextTokens((rc as { text: string }).text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -141,7 +146,7 @@ function getMessageTokenCount(message: Message): number {
|
||||
}
|
||||
if (b.is_error === true) tokens += 10
|
||||
} else if (b.type === 'thinking' && typeof b.thinking === 'string') {
|
||||
tokens += roughTokenCountEstimation(b.thinking)
|
||||
tokens += countTextTokens(b.thinking)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
@@ -303,4 +308,4 @@ export function getHybridStats(split: ContextSplit) {
|
||||
messageCount: split.cached.length + split.fresh.length,
|
||||
efficiency: split.totalTokens / (split.cachedTokens + split.freshTokens + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, beforeEach, afterEach, afterAll } from 'bun:test'
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
||||
import {
|
||||
addGlobalEntity,
|
||||
addGlobalSummary,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from './knowledgeGraph.js'
|
||||
import { mkdtempSync, rmSync, existsSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { dirname, join } from 'path'
|
||||
import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js'
|
||||
import { setClaudeConfigHomeDirForTesting } from './envUtils.js'
|
||||
import { getFsImplementation } from './fsOperations.js'
|
||||
@@ -18,8 +18,7 @@ import { getFsImplementation } from './fsOperations.js'
|
||||
describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const originalOrama = process.env.OPENCLAUDE_KNOWLEDGE_ORAMA
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-stress-'))
|
||||
const cwd = getFsImplementation().cwd()
|
||||
let configDir: string | undefined
|
||||
|
||||
const removeDirWithRetry = (dir: string) => {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
@@ -47,6 +46,7 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireEnvMutex()
|
||||
configDir = mkdtempSync(join(tmpdir(), 'openclaude-stress-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
process.env.OPENCLAUDE_KNOWLEDGE_ORAMA = '1'
|
||||
setClaudeConfigHomeDirForTesting(configDir)
|
||||
@@ -69,14 +69,18 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
}
|
||||
setClaudeConfigHomeDirForTesting(undefined)
|
||||
} finally {
|
||||
releaseEnvMutex()
|
||||
const dirToRemove = configDir
|
||||
configDir = undefined
|
||||
try {
|
||||
if (dirToRemove) {
|
||||
removeDirWithRetry(dirToRemove)
|
||||
}
|
||||
} finally {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
removeDirWithRetry(configDir)
|
||||
})
|
||||
|
||||
it('handles high-volume entity insertion (Stress Test)', async () => {
|
||||
const count = 50
|
||||
|
||||
@@ -109,6 +113,7 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
// 1. Create a valid DB
|
||||
await addGlobalEntity('type', 'valid', { val: '1' })
|
||||
const { getOramaPersistencePath } = await import('./knowledgeGraph.js')
|
||||
const cwd = getFsImplementation().cwd()
|
||||
const oramaPath = getOramaPersistencePath(cwd)
|
||||
expect(existsSync(oramaPath)).toBe(true)
|
||||
|
||||
@@ -126,8 +131,8 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
|
||||
// 5. Verify the corrupted file was moved
|
||||
const { readdirSync } = await import('fs')
|
||||
const projectsBaseDir = join(configDir, 'projects')
|
||||
expect(existsSync(projectsBaseDir)).toBe(true)
|
||||
const projectDir = dirname(oramaPath)
|
||||
expect(existsSync(projectDir)).toBe(true)
|
||||
// Search recursively for the corrupted file
|
||||
const findCorrupted = (dir: string): boolean => {
|
||||
const entries = readdirSync(dir, { withFileTypes: true })
|
||||
@@ -140,7 +145,7 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
|
||||
}
|
||||
return false
|
||||
}
|
||||
expect(findCorrupted(projectsBaseDir)).toBe(true)
|
||||
expect(findCorrupted(projectDir)).toBe(true)
|
||||
})
|
||||
|
||||
it('maintains consistency between JSON and Orama', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, beforeEach, afterEach, afterAll } from 'bun:test'
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
||||
import {
|
||||
addGlobalEntity,
|
||||
addGlobalRelation,
|
||||
@@ -18,8 +18,9 @@ import { sanitizePath } from './sessionStoragePortable.js'
|
||||
|
||||
describe('KnowledgeGraph Global Persistence & RAG', () => {
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-test-'))
|
||||
const cwd = process.cwd()
|
||||
let configDir: string | undefined
|
||||
|
||||
const removeDirWithRetry = (dir: string) => {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
@@ -46,6 +47,7 @@ describe('KnowledgeGraph Global Persistence & RAG', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireEnvMutex()
|
||||
configDir = mkdtempSync(join(tmpdir(), 'openclaude-test-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
setClaudeConfigHomeDirForTesting(configDir)
|
||||
resetGlobalGraph()
|
||||
@@ -62,14 +64,18 @@ describe('KnowledgeGraph Global Persistence & RAG', () => {
|
||||
}
|
||||
setClaudeConfigHomeDirForTesting(undefined)
|
||||
} finally {
|
||||
releaseEnvMutex()
|
||||
const dirToRemove = configDir
|
||||
configDir = undefined
|
||||
try {
|
||||
if (dirToRemove) {
|
||||
removeDirWithRetry(dirToRemove)
|
||||
}
|
||||
} finally {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
removeDirWithRetry(configDir)
|
||||
})
|
||||
|
||||
it('persists entities across loads', async () => {
|
||||
await addGlobalEntity('tool', 'openclaude', { status: 'alpha' })
|
||||
const path = getProjectGraphPath(cwd)
|
||||
|
||||
@@ -702,7 +702,7 @@ export function resetGlobalGraph(): void {
|
||||
join(projectDir, 'knowledge.db-wal'),
|
||||
join(projectDir, 'knowledge.db-shm'),
|
||||
]) {
|
||||
removePathWithRetry(sqlitePath)
|
||||
removePathWithRetry(sqlitePath, { requireMissingAfterCleanup: !sqliteCleared })
|
||||
}
|
||||
|
||||
const oramaPath = getOramaPersistencePath(cwd)
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
describe('getAgentModel provider-aware fallback', () => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/model/agent.test.ts')
|
||||
})
|
||||
|
||||
// Restore all mocks after each test
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('Claude-native providers', () => {
|
||||
@@ -258,4 +270,4 @@ describe('getAgentModel provider-aware fallback', () => {
|
||||
expect(checkIsClaudeNativeProvider()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { getCachedMiniMaxModelOptions } from './minimaxModels.js'
|
||||
|
||||
const ORIGINAL_MINIMAX_API_KEY = process.env.MINIMAX_API_KEY
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/model/minimaxModels.test.ts')
|
||||
process.env.MINIMAX_API_KEY = 'minimax-test'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_MINIMAX_API_KEY === undefined) {
|
||||
delete process.env.MINIMAX_API_KEY
|
||||
} else {
|
||||
process.env.MINIMAX_API_KEY = ORIGINAL_MINIMAX_API_KEY
|
||||
try {
|
||||
if (ORIGINAL_MINIMAX_API_KEY === undefined) {
|
||||
delete process.env.MINIMAX_API_KEY
|
||||
} else {
|
||||
process.env.MINIMAX_API_KEY = ORIGINAL_MINIMAX_API_KEY
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import { saveGlobalConfig } from '../config.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../config.js'
|
||||
import { getDefaultMainLoopModelSetting, getUserSpecifiedModelSetting } from './model.js'
|
||||
|
||||
const env = {
|
||||
@@ -12,8 +16,18 @@ const env = {
|
||||
CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY,
|
||||
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
||||
}
|
||||
const originalModel = getGlobalConfig().model
|
||||
|
||||
beforeEach(() => {
|
||||
function restoreEnv(key: keyof typeof env): void {
|
||||
if (env[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = env[key]
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('model/model.github.test.ts')
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
@@ -28,17 +42,17 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = env.CLAUDE_CODE_USE_GITHUB
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = env.CLAUDE_CODE_USE_OPENAI
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = env.CLAUDE_CODE_USE_GEMINI
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = env.CLAUDE_CODE_USE_BEDROCK
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = env.CLAUDE_CODE_USE_VERTEX
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = env.CLAUDE_CODE_USE_FOUNDRY
|
||||
process.env.OPENAI_MODEL = env.OPENAI_MODEL
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
model: undefined,
|
||||
}))
|
||||
try {
|
||||
for (const key of Object.keys(env) as Array<keyof typeof env>) {
|
||||
restoreEnv(key)
|
||||
}
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
model: originalModel,
|
||||
}))
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('github default model setting ignores non-string saved model', () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { saveGlobalConfig } from '../config.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../config.js'
|
||||
|
||||
async function importFreshModelModule() {
|
||||
mock.restore()
|
||||
@@ -52,6 +56,7 @@ const SAVED_ENV = {
|
||||
CODEX_API_KEY: process.env.CODEX_API_KEY,
|
||||
CHATGPT_ACCOUNT_ID: process.env.CHATGPT_ACCOUNT_ID,
|
||||
}
|
||||
const savedModel = getGlobalConfig().model
|
||||
|
||||
function restoreEnv(key: keyof typeof SAVED_ENV): void {
|
||||
if (SAVED_ENV[key] === undefined) {
|
||||
@@ -61,7 +66,8 @@ function restoreEnv(key: keyof typeof SAVED_ENV): void {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('model/model.openai-shim-providers.test.ts')
|
||||
// Other test files (notably modelOptions.github.test.ts) install a
|
||||
// persistent mock.module for './providers.js' that overrides getAPIProvider
|
||||
// globally. Without mock.restore() here, those overrides bleed into this
|
||||
@@ -88,14 +94,18 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
for (const key of Object.keys(SAVED_ENV) as Array<keyof typeof SAVED_ENV>) {
|
||||
restoreEnv(key)
|
||||
try {
|
||||
mock.restore()
|
||||
for (const key of Object.keys(SAVED_ENV) as Array<keyof typeof SAVED_ENV>) {
|
||||
restoreEnv(key)
|
||||
}
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
model: savedModel,
|
||||
}))
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
model: undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
test('codex provider reads OPENAI_MODEL, not stale settings.model', async () => {
|
||||
|
||||
@@ -2,7 +2,11 @@ import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
import { mock } from 'bun:test'
|
||||
|
||||
import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js'
|
||||
import { saveGlobalConfig } from '../config.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../config.js'
|
||||
import {
|
||||
resetSettingsCache,
|
||||
setSessionSettingsCache,
|
||||
@@ -32,6 +36,22 @@ const originalEnv = {
|
||||
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
||||
ANTHROPIC_CUSTOM_MODEL_OPTION: process.env.ANTHROPIC_CUSTOM_MODEL_OPTION,
|
||||
}
|
||||
const initialConfig = getGlobalConfig()
|
||||
const originalConfig = {
|
||||
additionalModelOptionsCache: structuredClone(
|
||||
initialConfig.additionalModelOptionsCache ?? [],
|
||||
),
|
||||
additionalModelOptionsCacheScope:
|
||||
initialConfig.additionalModelOptionsCacheScope,
|
||||
openaiAdditionalModelOptionsCache: structuredClone(
|
||||
initialConfig.openaiAdditionalModelOptionsCache ?? [],
|
||||
),
|
||||
openaiAdditionalModelOptionsCacheByProfile: structuredClone(
|
||||
initialConfig.openaiAdditionalModelOptionsCacheByProfile ?? {},
|
||||
),
|
||||
providerProfiles: structuredClone(initialConfig.providerProfiles ?? []),
|
||||
activeProviderProfileId: initialConfig.activeProviderProfileId,
|
||||
}
|
||||
|
||||
function restoreEnvValue(
|
||||
key: keyof typeof originalEnv,
|
||||
@@ -44,7 +64,8 @@ function restoreEnvValue(
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('model/modelOptions.github.test.ts')
|
||||
mock.restore()
|
||||
setSessionSettingsCache({ settings: {}, errors: [] })
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
@@ -60,27 +81,32 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
resetSettingsCache()
|
||||
restoreEnvValue('CLAUDE_CODE_USE_GITHUB')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_OPENAI')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_GEMINI')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_BEDROCK')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_VERTEX')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_FOUNDRY')
|
||||
restoreEnvValue('OPENAI_MODEL')
|
||||
restoreEnvValue('OPENAI_BASE_URL')
|
||||
restoreEnvValue('ANTHROPIC_CUSTOM_MODEL_OPTION')
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCache: [],
|
||||
additionalModelOptionsCacheScope: undefined,
|
||||
openaiAdditionalModelOptionsCache: [],
|
||||
openaiAdditionalModelOptionsCacheByProfile: {},
|
||||
providerProfiles: [],
|
||||
activeProviderProfileId: undefined,
|
||||
}))
|
||||
resetModelStringsForTestingOnly()
|
||||
try {
|
||||
mock.restore()
|
||||
resetSettingsCache()
|
||||
restoreEnvValue('CLAUDE_CODE_USE_GITHUB')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_OPENAI')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_GEMINI')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_BEDROCK')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_VERTEX')
|
||||
restoreEnvValue('CLAUDE_CODE_USE_FOUNDRY')
|
||||
restoreEnvValue('OPENAI_MODEL')
|
||||
restoreEnvValue('OPENAI_BASE_URL')
|
||||
restoreEnvValue('ANTHROPIC_CUSTOM_MODEL_OPTION')
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCache: originalConfig.additionalModelOptionsCache,
|
||||
additionalModelOptionsCacheScope: originalConfig.additionalModelOptionsCacheScope,
|
||||
openaiAdditionalModelOptionsCache: originalConfig.openaiAdditionalModelOptionsCache,
|
||||
openaiAdditionalModelOptionsCacheByProfile:
|
||||
originalConfig.openaiAdditionalModelOptionsCacheByProfile,
|
||||
providerProfiles: originalConfig.providerProfiles,
|
||||
activeProviderProfileId: originalConfig.activeProviderProfileId,
|
||||
}))
|
||||
resetModelStringsForTestingOnly()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('GitHub provider exposes default + all Copilot models in /model options', async () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { parseUserSpecifiedModel } from './model.js'
|
||||
import { getModelStrings } from './modelStrings.js'
|
||||
|
||||
@@ -22,14 +26,27 @@ function clearProviderFlags(): void {
|
||||
delete process.env.CLAUDE_CODE_USE_FOUNDRY
|
||||
}
|
||||
|
||||
function restoreEnv(key: keyof typeof originalEnv): void {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('model/modelStrings.github.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
|
||||
resetModelStringsForTestingOnly()
|
||||
try {
|
||||
for (const key of Object.keys(originalEnv) as Array<keyof typeof originalEnv>) {
|
||||
restoreEnv(key)
|
||||
}
|
||||
resetModelStringsForTestingOnly()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('GitHub provider model strings are concrete IDs', () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalAxiosGet = axios.get
|
||||
const originalEnv = {
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:
|
||||
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
|
||||
@@ -17,15 +22,24 @@ function restoreEnv(key: string, value: string | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/model/openaiModelDiscovery.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
originalEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
|
||||
)
|
||||
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
|
||||
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
|
||||
try {
|
||||
mock.restore()
|
||||
axios.get = originalAxiosGet
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
originalEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
|
||||
)
|
||||
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
|
||||
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('skips legacy OpenAI-compatible model discovery when nonessential traffic is disabled', async () => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const originalEnv = {
|
||||
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
|
||||
@@ -13,21 +18,31 @@ const originalEnv = {
|
||||
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
|
||||
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
||||
XAI_API_KEY: process.env.XAI_API_KEY,
|
||||
VENICE_API_KEY: process.env.VENICE_API_KEY,
|
||||
MIMO_API_KEY: process.env.MIMO_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
}
|
||||
|
||||
function restoreEnv(key: keyof typeof originalEnv): void {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('model/providers.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
|
||||
process.env.NVIDIA_NIM = originalEnv.NVIDIA_NIM
|
||||
process.env.MINIMAX_API_KEY = originalEnv.MINIMAX_API_KEY
|
||||
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
|
||||
process.env.OPENAI_API_BASE = originalEnv.OPENAI_API_BASE
|
||||
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
|
||||
process.env.XAI_API_KEY = originalEnv.XAI_API_KEY
|
||||
try {
|
||||
for (const key of Object.keys(originalEnv) as Array<keyof typeof originalEnv>) {
|
||||
restoreEnv(key)
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function importFreshProvidersModule() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, beforeEach } from 'bun:test'
|
||||
import { afterEach, describe, expect, it, beforeEach } from 'bun:test'
|
||||
import {
|
||||
startNewTurn,
|
||||
getCurrentTurn,
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
resetMultiTurnState,
|
||||
createMultiTurnTracker,
|
||||
} from './multiTurnContext.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
function createMessage(role: string, content: string): any {
|
||||
return {
|
||||
@@ -21,10 +25,21 @@ function createMessage(role: string, content: string): any {
|
||||
}
|
||||
|
||||
describe('multiTurnContext', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/multiTurnContext.test.ts')
|
||||
createMultiTurnTracker()
|
||||
resetMultiTurnState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
createMultiTurnTracker()
|
||||
resetMultiTurnState()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('startNewTurn', () => {
|
||||
it('creates a new turn', () => {
|
||||
const turn = startNewTurn()
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import * as fsPromises from 'fs/promises'
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realEnv from './env.js'
|
||||
import * as realEnvUtils from './envUtils.js'
|
||||
import * as realExecFileNoThrow from './execFileNoThrow.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/openclaudeInstallSurfaces.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
mock.restore()
|
||||
try {
|
||||
process.env = { ...originalEnv }
|
||||
if (originalMacro === undefined) {
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
} else {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
}
|
||||
mock.restore()
|
||||
mock.module('../utils/env.js', () => realEnv)
|
||||
mock.module('./envUtils.js', () => realEnvUtils)
|
||||
mock.module('./execFileNoThrow.js', () => realExecFileNoThrow)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function importFreshInstallCommand() {
|
||||
@@ -22,6 +44,7 @@ async function importFreshInstaller() {
|
||||
|
||||
test('install command displays ~/.local/bin/openclaude on non-Windows', async () => {
|
||||
mock.module('../utils/env.js', () => ({
|
||||
...realEnv,
|
||||
env: { platform: 'darwin' },
|
||||
}))
|
||||
|
||||
@@ -32,6 +55,7 @@ test('install command displays ~/.local/bin/openclaude on non-Windows', async ()
|
||||
|
||||
test('install command displays openclaude.exe path on Windows', async () => {
|
||||
mock.module('../utils/env.js', () => ({
|
||||
...realEnv,
|
||||
env: { platform: 'win32' },
|
||||
}))
|
||||
|
||||
@@ -56,6 +80,7 @@ test('cleanupNpmInstallations removes both openclaude and legacy claude local in
|
||||
}))
|
||||
|
||||
mock.module('./execFileNoThrow.js', () => ({
|
||||
...realExecFileNoThrow,
|
||||
execFileNoThrowWithCwd: async () => ({
|
||||
code: 1,
|
||||
stderr: 'npm ERR! code E404',
|
||||
@@ -63,6 +88,7 @@ test('cleanupNpmInstallations removes both openclaude and legacy claude local in
|
||||
}))
|
||||
|
||||
mock.module('./envUtils.js', () => ({
|
||||
...realEnvUtils,
|
||||
getClaudeConfigHomeDir: () => join(homedir(), '.openclaude'),
|
||||
isEnvTruthy: (value: string | undefined) => value === '1',
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import {
|
||||
__resetGitEnvWarningForTesting,
|
||||
buildGitChildEnv,
|
||||
@@ -62,16 +66,21 @@ describe('buildGitChildEnv', () => {
|
||||
const ORIGINAL_BAD_KEY = 'OPENCLAUDE_TEST_BAD_ENV_FOR_GIT'
|
||||
let originalValue: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/plugins/gitEnv.test.ts')
|
||||
__resetGitEnvWarningForTesting()
|
||||
originalValue = process.env[ORIGINAL_BAD_KEY]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalValue === undefined) {
|
||||
delete process.env[ORIGINAL_BAD_KEY]
|
||||
} else {
|
||||
process.env[ORIGINAL_BAD_KEY] = originalValue
|
||||
try {
|
||||
if (originalValue === undefined) {
|
||||
delete process.env[ORIGINAL_BAD_KEY]
|
||||
} else {
|
||||
process.env[ORIGINAL_BAD_KEY] = originalValue
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
type MarketplaceEntry = {
|
||||
name: string
|
||||
@@ -23,7 +27,14 @@ let config = {
|
||||
}
|
||||
let addMarketplaceSourceFn = mock(() => {})
|
||||
|
||||
await acquireSharedMutationLock('utils/plugins/lspRecommendation.test.ts')
|
||||
|
||||
mock.module('./marketplaceManager.js', () => ({
|
||||
addMarketplaceSource: addMarketplaceSourceFn,
|
||||
getMarketplaceCacheOnly: async (name: string) => ({
|
||||
plugins: marketplaces[name] ?? [],
|
||||
}),
|
||||
getMarketplacesCacheDir: () => '/tmp/openclaude-marketplaces',
|
||||
loadKnownMarketplacesConfig: async () =>
|
||||
Object.fromEntries(
|
||||
Object.keys(marketplaces).map(name => [
|
||||
@@ -31,10 +42,19 @@ mock.module('./marketplaceManager.js', () => ({
|
||||
{ installLocation: `/tmp/${name}` },
|
||||
]),
|
||||
),
|
||||
loadKnownMarketplacesConfigSafe: async () =>
|
||||
Object.fromEntries(
|
||||
Object.keys(marketplaces).map(name => [
|
||||
name,
|
||||
{ installLocation: `/tmp/${name}` },
|
||||
]),
|
||||
),
|
||||
getMarketplace: async (name: string) => ({
|
||||
plugins: marketplaces[name] ?? [],
|
||||
}),
|
||||
addMarketplaceSource: addMarketplaceSourceFn,
|
||||
getPluginById: async () => undefined,
|
||||
getPluginByIdCacheOnly: async () => undefined,
|
||||
saveKnownMarketplacesConfig: mock(async () => {}),
|
||||
}))
|
||||
|
||||
mock.module('../binaryCheck.js', () => ({
|
||||
@@ -42,20 +62,92 @@ mock.module('../binaryCheck.js', () => ({
|
||||
}))
|
||||
|
||||
mock.module('./installedPluginsManager.js', () => ({
|
||||
addInstalledPlugin: mock(() => {}),
|
||||
addPluginInstallation: mock(() => {}),
|
||||
clearInstalledPluginsCache: mock(() => {}),
|
||||
getInMemoryInstalledPlugins: () => ({ installations: {}, schemaVersion: 2 }),
|
||||
getGitCommitSha: async () => undefined,
|
||||
getPendingUpdateCount: () => 0,
|
||||
getPendingUpdatesDetails: () => [],
|
||||
hasPendingUpdates: () => false,
|
||||
loadInstalledPluginsFromDisk: () => ({
|
||||
installations: {},
|
||||
schemaVersion: 2,
|
||||
}),
|
||||
removeAllPluginsForMarketplace: () => ({ removed: 0 }),
|
||||
removeInstalledPlugin: mock(() => {}),
|
||||
removePluginInstallation: mock(() => {}),
|
||||
resetInMemoryState: mock(() => {}),
|
||||
isPluginInstalled: (pluginId: string) => installedPlugins.has(pluginId),
|
||||
isPluginGloballyInstalled: (pluginId: string) => installedPlugins.has(pluginId),
|
||||
updateInstallationPathOnDisk: mock(() => {}),
|
||||
}))
|
||||
|
||||
mock.module('../config.js', () => ({
|
||||
checkHasTrustDialogAccepted: () => true,
|
||||
enableConfigs: mock(() => {}),
|
||||
getCurrentProjectConfig: () => ({}),
|
||||
getGlobalConfig: () => config,
|
||||
getGlobalConfigWriteCount: () => 0,
|
||||
getAutoUpdaterDisabledReason: () => null,
|
||||
formatAutoUpdaterDisabledReason: () => 'enabled',
|
||||
getManagedClaudeRulesDir: () => '/tmp/openclaude-managed-rules',
|
||||
getMemoryPath: () => '/tmp/openclaude-memory.md',
|
||||
getOrCreateUserID: () => 'test-user-id',
|
||||
getProjectPathForConfig: () => '/tmp/openclaude-project-config.json',
|
||||
getRemoteControlAtStartup: () => false,
|
||||
getUserClaudeRulesDir: () => '/tmp/openclaude-user-rules',
|
||||
isAutoUpdaterDisabled: () => false,
|
||||
recordFirstStartTime: mock(() => {}),
|
||||
getCustomApiKeyStatus: () => ({ hasCustomApiKey: false }),
|
||||
isGlobalConfigKey: () => false,
|
||||
isPathTrusted: () => true,
|
||||
isProjectConfigKey: () => false,
|
||||
resetTrustDialogAcceptedCacheForTesting: mock(() => {}),
|
||||
shouldSkipPluginAutoupdate: () => false,
|
||||
saveGlobalConfig: mock((updater: (current: typeof config) => typeof config) => {
|
||||
config = updater(config)
|
||||
}),
|
||||
saveCurrentProjectConfig: mock(() => {}),
|
||||
}))
|
||||
|
||||
const {
|
||||
getMatchingLspPlugins,
|
||||
listLspPluginCandidates,
|
||||
} = await import('./lspRecommendation.js')
|
||||
let getMatchingLspPlugins: typeof import('./lspRecommendation.js').getMatchingLspPlugins
|
||||
let listLspPluginCandidates: typeof import('./lspRecommendation.js').listLspPluginCandidates
|
||||
|
||||
function resetTestState(): void {
|
||||
marketplaces = {
|
||||
'claude-plugins-official': [
|
||||
lspPlugin('typescript-lsp', 'typescript-language-server', [
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
]),
|
||||
lspPlugin('pyright-lsp', 'pyright-langserver', ['.py', '.pyi']),
|
||||
],
|
||||
community: [lspPlugin('rust-analyzer-lsp', 'rust-analyzer', ['.rs'])],
|
||||
}
|
||||
installedPlugins = new Set()
|
||||
installedBinaries = new Set(['typescript-language-server'])
|
||||
config = {
|
||||
lspRecommendationDisabled: false,
|
||||
lspRecommendationNeverPlugins: [],
|
||||
lspRecommendationIgnoredCount: 0,
|
||||
}
|
||||
addMarketplaceSourceFn.mockClear()
|
||||
}
|
||||
|
||||
resetTestState()
|
||||
const mod = await import('./lspRecommendation.js')
|
||||
getMatchingLspPlugins = mod.getMatchingLspPlugins
|
||||
listLspPluginCandidates = mod.listLspPluginCandidates
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
function lspPlugin(
|
||||
name: string,
|
||||
@@ -78,25 +170,7 @@ function lspPlugin(
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
marketplaces = {
|
||||
'claude-plugins-official': [
|
||||
lspPlugin('typescript-lsp', 'typescript-language-server', [
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
]),
|
||||
lspPlugin('pyright-lsp', 'pyright-langserver', ['.py', '.pyi']),
|
||||
],
|
||||
community: [lspPlugin('rust-analyzer-lsp', 'rust-analyzer', ['.rs'])],
|
||||
}
|
||||
installedPlugins = new Set()
|
||||
installedBinaries = new Set(['typescript-language-server'])
|
||||
config = {
|
||||
lspRecommendationDisabled: false,
|
||||
lspRecommendationNeverPlugins: [],
|
||||
lspRecommendationIgnoredCount: 0,
|
||||
}
|
||||
addMarketplaceSourceFn.mockClear()
|
||||
resetTestState()
|
||||
})
|
||||
|
||||
describe('listLspPluginCandidates', () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
type TestGlobalConfig = {
|
||||
officialMarketplaceAutoInstallAttempted?: boolean
|
||||
@@ -32,6 +36,8 @@ const addMarketplaceSource = mock(async () => ({
|
||||
resolvedSource: {},
|
||||
}))
|
||||
|
||||
await acquireSharedMutationLock('utils/plugins/officialMarketplaceStartupCheck.test.ts')
|
||||
|
||||
mock.module('../../services/analytics/growthbook.js', () => ({
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: () => true,
|
||||
}))
|
||||
@@ -41,8 +47,29 @@ mock.module('../../services/analytics/index.js', () => ({
|
||||
}))
|
||||
|
||||
mock.module('../config.js', () => ({
|
||||
checkHasTrustDialogAccepted: () => true,
|
||||
enableConfigs: mock(() => {}),
|
||||
getCurrentProjectConfig: () => ({}),
|
||||
getGlobalConfig: () => config,
|
||||
getGlobalConfigWriteCount: () => 0,
|
||||
getAutoUpdaterDisabledReason: () => null,
|
||||
formatAutoUpdaterDisabledReason: () => 'enabled',
|
||||
getManagedClaudeRulesDir: () => '/tmp/openclaude-managed-rules',
|
||||
getMemoryPath: () => '/tmp/openclaude-memory.md',
|
||||
getOrCreateUserID: () => 'test-user-id',
|
||||
getProjectPathForConfig: () => '/tmp/openclaude-project-config.json',
|
||||
getRemoteControlAtStartup: () => false,
|
||||
getUserClaudeRulesDir: () => '/tmp/openclaude-user-rules',
|
||||
isAutoUpdaterDisabled: () => false,
|
||||
recordFirstStartTime: mock(() => {}),
|
||||
getCustomApiKeyStatus: () => ({ hasCustomApiKey: false }),
|
||||
isGlobalConfigKey: () => false,
|
||||
isPathTrusted: () => true,
|
||||
isProjectConfigKey: () => false,
|
||||
resetTrustDialogAcceptedCacheForTesting: mock(() => {}),
|
||||
shouldSkipPluginAutoupdate: () => false,
|
||||
saveGlobalConfig,
|
||||
saveCurrentProjectConfig: mock(() => {}),
|
||||
}))
|
||||
|
||||
mock.module('../debug.js', () => ({
|
||||
@@ -64,8 +91,13 @@ mock.module('./marketplaceHelpers.js', () => ({
|
||||
|
||||
mock.module('./marketplaceManager.js', () => ({
|
||||
addMarketplaceSource,
|
||||
getMarketplace: async () => ({ plugins: [] }),
|
||||
getMarketplaceCacheOnly: async () => ({ plugins: [] }),
|
||||
getMarketplacesCacheDir: () => '/tmp/openclaude-marketplaces',
|
||||
getPluginById: async () => undefined,
|
||||
getPluginByIdCacheOnly: async () => undefined,
|
||||
loadKnownMarketplacesConfig: async () => knownMarketplaces,
|
||||
loadKnownMarketplacesConfigSafe: async () => knownMarketplaces,
|
||||
saveKnownMarketplacesConfig,
|
||||
}))
|
||||
|
||||
@@ -73,9 +105,19 @@ mock.module('./officialMarketplaceGcs.js', () => ({
|
||||
fetchOfficialMarketplaceFromGcs,
|
||||
}))
|
||||
|
||||
const { checkAndInstallOfficialMarketplace } = await import(
|
||||
'./officialMarketplaceStartupCheck.js'
|
||||
)
|
||||
let checkAndInstallOfficialMarketplace:
|
||||
typeof import('./officialMarketplaceStartupCheck.js').checkAndInstallOfficialMarketplace
|
||||
|
||||
const mod = await import('./officialMarketplaceStartupCheck.js')
|
||||
checkAndInstallOfficialMarketplace = mod.checkAndInstallOfficialMarketplace
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
config = {}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { setInlinePlugins } from '../../bootstrap/state.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import type { LoadedPlugin } from '../../types/plugin.js'
|
||||
import {
|
||||
clearPluginCache,
|
||||
@@ -14,10 +18,18 @@ import {
|
||||
} from './pluginLoader.js'
|
||||
import { clearPluginSkillsCache, getPluginSkills } from './loadPluginCommands.js'
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/plugins/pluginLoader.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setInlinePlugins([])
|
||||
clearPluginCache('pluginLoader.test cleanup')
|
||||
clearPluginSkillsCache()
|
||||
try {
|
||||
setInlinePlugins([])
|
||||
clearPluginCache('pluginLoader.test cleanup')
|
||||
clearPluginSkillsCache()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
function marketplacePlugin(
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
import { getEmptyToolPermissionContext } from '../Tool.js'
|
||||
import { BashTool } from '../tools/BashTool/BashTool.js'
|
||||
import { executeShellCommandsInPrompt } from './promptShellExecution.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const originalCall = BashTool.call
|
||||
const originalMapToolResultToToolResultBlockParam =
|
||||
BashTool.mapToolResultToToolResultBlockParam
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/promptShellExecution.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
BashTool.call = originalCall
|
||||
BashTool.mapToolResultToToolResultBlockParam =
|
||||
originalMapToolResultToToolResultBlockParam
|
||||
try {
|
||||
BashTool.call = originalCall
|
||||
BashTool.mapToolResultToToolResultBlockParam =
|
||||
originalMapToolResultToToolResultBlockParam
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('executeShellCommandsInPrompt normalizes null shell output', async () => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
async function loadProviderDiscoveryModule() {
|
||||
// @ts-expect-error cache-busting query string for Bun module mocks
|
||||
@@ -10,9 +15,26 @@ const originalEnv = {
|
||||
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
||||
}
|
||||
|
||||
function restoreEnv(key: keyof typeof originalEnv): void {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('providerDiscovery.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
|
||||
try {
|
||||
mock.restore()
|
||||
globalThis.fetch = originalFetch
|
||||
restoreEnv('OPENAI_BASE_URL')
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('lists models from a local openai-compatible /models endpoint', async () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import {
|
||||
parseProviderFlag,
|
||||
parseModelFlag,
|
||||
@@ -32,7 +36,8 @@ const ENV_KEYS = [
|
||||
|
||||
const originalEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/providerFlag.test.ts')
|
||||
for (const key of ENV_KEYS) {
|
||||
originalEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
@@ -68,12 +73,16 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
try {
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, beforeAll, beforeEach, expect, test } from 'bun:test'
|
||||
import { ensureIntegrationsLoaded, getAllGateways } from '../integrations/index.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
import {
|
||||
getProviderValidationError,
|
||||
@@ -41,7 +45,8 @@ beforeAll(() => {
|
||||
ensureIntegrationsLoaded()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/providerValidation.test.ts')
|
||||
for (const key of ENV_KEYS) {
|
||||
originalEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
@@ -49,12 +54,16 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
try {
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
|
||||
import { expect, test, mock, describe, beforeEach, afterEach } from "bun:test";
|
||||
import { linuxSecretStorage } from "./linuxSecretStorage.js";
|
||||
import { windowsCredentialStorage } from "./windowsCredentialStorage.js";
|
||||
import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test";
|
||||
import * as realExeca from "execa";
|
||||
import { getSecureStorageServiceName, CREDENTIALS_SERVICE_SUFFIX } from "./macOsKeychainHelpers.js";
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from "../../test/sharedMutationLock.js";
|
||||
import type { linuxSecretStorage as LinuxSecretStorage } from "./linuxSecretStorage.js";
|
||||
import type { windowsCredentialStorage as WindowsCredentialStorage } from "./windowsCredentialStorage.js";
|
||||
|
||||
// Mock execaSync
|
||||
const mockExecaSync = mock(() => ({ exitCode: 0, stdout: "" }));
|
||||
mock.module("execa", () => ({
|
||||
execaSync: mockExecaSync,
|
||||
}));
|
||||
|
||||
describe("Secure Storage Platform Implementations", () => {
|
||||
const originalEnv = process.env;
|
||||
let linuxSecretStorage: typeof LinuxSecretStorage;
|
||||
let windowsCredentialStorage: typeof WindowsCredentialStorage;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock("platformStorage.test.ts");
|
||||
mock.module("execa", () => ({
|
||||
...realExeca,
|
||||
execaSync: mockExecaSync,
|
||||
}));
|
||||
({ linuxSecretStorage } = await import("./linuxSecretStorage.js"));
|
||||
({ windowsCredentialStorage } = await import("./windowsCredentialStorage.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
mockExecaSync.mockClear();
|
||||
// Default mock behavior
|
||||
@@ -26,8 +35,13 @@ describe("Secure Storage Platform Implementations", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
process.env = originalEnv;
|
||||
mock.restore();
|
||||
mock.module("execa", () => realExeca);
|
||||
} finally {
|
||||
releaseSharedMutationLock();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { DEFAULT_CODEX_BASE_URL } from '../services/api/providerConfig.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env }
|
||||
|
||||
@@ -41,9 +45,17 @@ async function readPropertyValue(
|
||||
?.value
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/status.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
restoreEnv()
|
||||
try {
|
||||
mock.restore()
|
||||
restoreEnv()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('buildAPIProviderProperties labels NVIDIA NIM sessions', async () => {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
||||
import { join } from 'path'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { JSONProvider } from './JSONProvider.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
|
||||
@@ -31,11 +35,19 @@ function captureConsoleError<T>(run: () => T): { result: T; calls: unknown[][] }
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/storage/JSONProvider.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop()
|
||||
if (!dir) continue
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
try {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop()
|
||||
if (!dir) continue
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('SQLite Storage Layer', () => {
|
||||
expect(Object.keys(graph.entities).length).toBe(count)
|
||||
})
|
||||
|
||||
it('does not report a closed on-disk database as cleared', async () => {
|
||||
it('clears a closed on-disk database without an existing provider handle', async () => {
|
||||
const projectDir = join(getProjectsDir(), sanitizePath(workspaceDir))
|
||||
const sqlitePath = join(projectDir, 'knowledge.db')
|
||||
|
||||
@@ -185,6 +185,10 @@ describe('SQLite Storage Layer', () => {
|
||||
clearMemoryOnly()
|
||||
|
||||
const closedProvider = new SQLiteProvider(projectDir)
|
||||
expect(closedProvider.clear()).toBe(false)
|
||||
expect(closedProvider.clear()).toBe(true)
|
||||
|
||||
await closedProvider.init()
|
||||
expect(closedProvider.loadGraph()).toBeNull()
|
||||
closedProvider.close()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -261,7 +261,27 @@ export class SQLiteProvider {
|
||||
|
||||
public clear(): boolean {
|
||||
if (!this.db) {
|
||||
return !existsSync(this.dbPath)
|
||||
if (!existsSync(this.dbPath)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof Bun === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const { Database } = require('bun:sqlite')
|
||||
this.db = new Database(this.dbPath)
|
||||
this.db.exec('PRAGMA journal_mode = WAL;')
|
||||
this.db.exec('PRAGMA foreign_keys = ON;')
|
||||
this.createTables()
|
||||
return this.clear()
|
||||
} catch (e) {
|
||||
console.error('Failed to open SQLite knowledge graph for clearing:', e)
|
||||
return false
|
||||
} finally {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import { buildInheritedEnvVars } from './spawnUtils.js'
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env }
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/swarm/spawnUtils.test.ts')
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key]
|
||||
try {
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key]
|
||||
}
|
||||
Object.assign(process.env, ORIGINAL_ENV)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
Object.assign(process.env, ORIGINAL_ENV)
|
||||
})
|
||||
|
||||
test('buildInheritedEnvVars marks spawned teammates as host-managed for provider routing', () => {
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { afterEach, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/swarm/teammateModel.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
try {
|
||||
mock.restore()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function importFreshTeammateModelModule(provider = 'mistral') {
|
||||
mock.restore()
|
||||
mock.module('../model/providers.js', () => ({
|
||||
getAPIProvider: () => provider,
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import { resetSettingsCache } from './settings/settingsCache.js'
|
||||
|
||||
const ENV_KEYS = [
|
||||
@@ -26,7 +30,8 @@ const ENV_KEYS = [
|
||||
|
||||
const originalEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/thinking.test.ts')
|
||||
for (const key of ENV_KEYS) {
|
||||
originalEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
@@ -35,15 +40,19 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
try {
|
||||
mock.restore()
|
||||
for (const key of ENV_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
resetSettingsCache()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
resetSettingsCache()
|
||||
})
|
||||
|
||||
async function importFreshThinkingModule() {
|
||||
|
||||
+39
-4
@@ -1,6 +1,17 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import * as realExeca from 'execa'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import * as realAuth from './auth.js'
|
||||
import * as realConfig from './config.js'
|
||||
import * as realCwd from './cwd.js'
|
||||
import * as realEnv from './env.js'
|
||||
import * as realEnvUtils from './envUtils.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
|
||||
async function importFreshUserModule() {
|
||||
return import(`./user.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
@@ -18,6 +29,7 @@ function installCommonMocks(options?: {
|
||||
// which is fine — these tests only assert email, not sessionId.
|
||||
|
||||
mock.module('./auth.js', () => ({
|
||||
...realAuth,
|
||||
getOauthAccountInfo: () =>
|
||||
options?.oauthEmail
|
||||
? {
|
||||
@@ -31,25 +43,30 @@ function installCommonMocks(options?: {
|
||||
}))
|
||||
|
||||
mock.module('./config.js', () => ({
|
||||
...realConfig,
|
||||
getGlobalConfig: () => ({}),
|
||||
getOrCreateUserID: () => 'device-test',
|
||||
}))
|
||||
|
||||
mock.module('./cwd.js', () => ({
|
||||
...realCwd,
|
||||
getCwd: () => 'C:\\repo',
|
||||
}))
|
||||
|
||||
mock.module('./env.js', () => ({
|
||||
...realEnv,
|
||||
env: { platform: 'windows' },
|
||||
getHostPlatformForAnalytics: () => 'windows',
|
||||
}))
|
||||
|
||||
mock.module('./envUtils.js', () => ({
|
||||
...realEnvUtils,
|
||||
isEnvTruthy: (value: string | undefined) =>
|
||||
!!value && value !== '0' && value.toLowerCase() !== 'false',
|
||||
}))
|
||||
|
||||
mock.module('execa', () => ({
|
||||
...realExeca,
|
||||
execa: async () => ({
|
||||
exitCode: options?.gitEmail ? 0 : 1,
|
||||
stdout: options?.gitEmail ?? '',
|
||||
@@ -57,10 +74,28 @@ function installCommonMocks(options?: {
|
||||
}))
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/user.test.ts')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
try {
|
||||
mock.restore()
|
||||
mock.module('./auth.js', () => realAuth)
|
||||
mock.module('./config.js', () => realConfig)
|
||||
mock.module('./cwd.js', () => realCwd)
|
||||
mock.module('./env.js', () => realEnv)
|
||||
mock.module('./envUtils.js', () => realEnvUtils)
|
||||
mock.module('execa', () => realExeca)
|
||||
process.env = { ...originalEnv }
|
||||
if (originalMacro === undefined) {
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
} else {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('user email fallbacks', () => {
|
||||
|
||||
@@ -2,19 +2,28 @@ import { describe, test, expect, afterEach, beforeAll, afterAll } from 'bun:test
|
||||
import {
|
||||
unstable_v2_createSession,
|
||||
} from '../../src/entrypoints/sdk/index.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
|
||||
// sendMessage drains trigger init(), which checks auth. Stub it for CI.
|
||||
const AUTH_KEY = 'ANTHROPIC_API_KEY'
|
||||
let savedApiKey: string | undefined
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/engine-mutators.test.ts')
|
||||
savedApiKey = process.env[AUTH_KEY]
|
||||
if (!savedApiKey) process.env[AUTH_KEY] = 'sk-test-engine-mutators-stub'
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
try {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
import { QueryEngine } from '../../src/QueryEngine.js'
|
||||
import type { QueryEngineConfig } from '../../src/QueryEngine.js'
|
||||
|
||||
@@ -2,21 +2,44 @@ import { describe, test, expect, vi, beforeEach, afterEach, beforeAll, afterAll
|
||||
import { unstable_v2_createSession } from '../../src/entrypoints/sdk/index.js'
|
||||
import { query } from '../../src/entrypoints/sdk/index.js'
|
||||
import type { MCPServerConnection } from '../../src/services/mcp/types.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
|
||||
// sendMessage drains trigger init(), which checks auth. Stub it for CI.
|
||||
const AUTH_KEY = 'ANTHROPIC_API_KEY'
|
||||
let savedApiKey: string | undefined
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/mcp-cleanup.test.ts')
|
||||
savedApiKey = process.env[AUTH_KEY]
|
||||
if (!savedApiKey) process.env[AUTH_KEY] = 'sk-test-mcp-cleanup-stub'
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
try {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function waitForMockCall(
|
||||
mockFn: ReturnType<typeof vi.fn>,
|
||||
timeoutMs = 1000,
|
||||
): Promise<void> {
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (mockFn.mock.calls.length > 0) {
|
||||
return
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5))
|
||||
}
|
||||
throw new Error('Timed out waiting for MCP cleanup mock call')
|
||||
}
|
||||
|
||||
describe('MCP cleanup on session close', () => {
|
||||
test('session.close() disconnects MCP clients', async () => {
|
||||
// Create a mock MCP client with a cleanup method
|
||||
@@ -44,8 +67,7 @@ describe('MCP cleanup on session close', () => {
|
||||
// Close the session
|
||||
session.close()
|
||||
|
||||
// Verify MCP client cleanup was called (fire-and-forget, wait a bit)
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await waitForMockCall(mockCleanup)
|
||||
expect(mockCleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -74,8 +96,7 @@ describe('MCP cleanup on session close', () => {
|
||||
// Close should not throw despite MCP cleanup error
|
||||
expect(() => session.close()).not.toThrow()
|
||||
|
||||
// Verify cleanup was attempted even though it will reject
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await waitForMockCall(mockCleanup)
|
||||
expect(mockCleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -156,8 +177,7 @@ describe('MCP cleanup on query close', () => {
|
||||
|
||||
q.close()
|
||||
|
||||
// Verify cleanup was called (fire-and-forget, wait a bit)
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await waitForMockCall(mockCleanup)
|
||||
expect(mockCleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -183,7 +203,7 @@ describe('MCP cleanup on query close', () => {
|
||||
}
|
||||
|
||||
expect(() => q.close()).not.toThrow()
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await waitForMockCall(mockCleanup)
|
||||
expect(mockCleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -214,4 +234,4 @@ describe('MCP cleanup on query close', () => {
|
||||
|
||||
expect(() => q.close()).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
||||
import { MockQueryEngine } from './helpers/mock-engine.js'
|
||||
import { query } from '../../src/entrypoints/sdk/index.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No mock.module() — avoids module-cache leakage across test files.
|
||||
@@ -12,7 +16,8 @@ import { query } from '../../src/entrypoints/sdk/index.js'
|
||||
const AUTH_KEY = 'ANTHROPIC_API_KEY'
|
||||
let savedApiKey: string | undefined
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/query-happy-path.test.ts')
|
||||
savedApiKey = process.env[AUTH_KEY]
|
||||
if (!savedApiKey) {
|
||||
process.env[AUTH_KEY] = 'sk-test-happy-path-stub'
|
||||
@@ -20,10 +25,14 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (savedApiKey === undefined) {
|
||||
delete process.env[AUTH_KEY]
|
||||
} else {
|
||||
process.env[AUTH_KEY] = savedApiKey
|
||||
try {
|
||||
if (savedApiKey === undefined) {
|
||||
delete process.env[AUTH_KEY]
|
||||
} else {
|
||||
process.env[AUTH_KEY] = savedApiKey
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -10,13 +10,18 @@ import {
|
||||
createMultiTurnConversation,
|
||||
UUID_REGEX,
|
||||
} from './helpers/query-test-doubles.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
|
||||
// Tests that drain fully (no early interrupt) trigger init(), which checks
|
||||
// for auth credentials. Provide a stub key so init() succeeds without network.
|
||||
const AUTH_KEY = 'ANTHROPIC_API_KEY'
|
||||
let savedApiKey: string | undefined
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/query-lifecycle.test.ts')
|
||||
savedApiKey = process.env[AUTH_KEY]
|
||||
if (!savedApiKey) {
|
||||
process.env[AUTH_KEY] = 'sk-test-lifecycle-stub'
|
||||
@@ -24,10 +29,14 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (savedApiKey === undefined) {
|
||||
delete process.env[AUTH_KEY]
|
||||
} else {
|
||||
process.env[AUTH_KEY] = savedApiKey
|
||||
try {
|
||||
if (savedApiKey === undefined) {
|
||||
delete process.env[AUTH_KEY]
|
||||
} else {
|
||||
process.env[AUTH_KEY] = savedApiKey
|
||||
}
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
||||
import { query } from '../../src/entrypoints/sdk/index.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
|
||||
// These tests don't iterate — they test QueryImpl methods that manipulate
|
||||
// internal state. Auth stub needed because query() triggers init() path.
|
||||
const AUTH_KEY = 'ANTHROPIC_API_KEY'
|
||||
let savedApiKey: string | undefined
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/query-methods.test.ts')
|
||||
savedApiKey = process.env[AUTH_KEY]
|
||||
if (!savedApiKey) process.env[AUTH_KEY] = 'sk-test-query-methods-stub'
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
try {
|
||||
if (savedApiKey === undefined) delete process.env[AUTH_KEY]
|
||||
else process.env[AUTH_KEY] = savedApiKey
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
describe('QueryImpl.setModel', () => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import {
|
||||
assertValidSessionId,
|
||||
mapMessageToSDK,
|
||||
acquireEnvMutex,
|
||||
releaseEnvMutex,
|
||||
resetEnvMutexForTesting,
|
||||
createEnvMutexForTesting,
|
||||
} from '../../src/entrypoints/sdk/shared.js'
|
||||
|
||||
describe('assertValidSessionId', () => {
|
||||
@@ -83,69 +81,87 @@ describe('mapMessageToSDK', () => {
|
||||
})
|
||||
|
||||
describe.serial('env mutex timeout', () => {
|
||||
beforeEach(() => {
|
||||
resetEnvMutexForTesting()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetEnvMutexForTesting()
|
||||
})
|
||||
|
||||
test('acquireEnvMutex returns timeout result when mutex is locked', async () => {
|
||||
const { acquireEnvMutex, releaseEnvMutex } = createEnvMutexForTesting()
|
||||
// First acquire locks the mutex
|
||||
const firstResult = await acquireEnvMutex()
|
||||
const firstResult = await acquireEnvMutex({ timeoutMs: 5_000 })
|
||||
expect(firstResult.acquired).toBe(true)
|
||||
|
||||
// Second acquire with timeout should return timeout result
|
||||
const secondResult = await acquireEnvMutex({ timeoutMs: 100 })
|
||||
expect(secondResult.acquired).toBe(false)
|
||||
expect(secondResult.reason).toBe('timeout')
|
||||
|
||||
// Clean up
|
||||
releaseEnvMutex()
|
||||
try {
|
||||
const secondResult = await acquireEnvMutex({ timeoutMs: 100 })
|
||||
expect(secondResult.acquired).toBe(false)
|
||||
expect(secondResult.reason).toBe('timeout')
|
||||
} finally {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
})
|
||||
|
||||
test('acquireEnvMutex succeeds before timeout', async () => {
|
||||
await acquireEnvMutex()
|
||||
const { acquireEnvMutex, releaseEnvMutex } = createEnvMutexForTesting()
|
||||
const firstResult = await acquireEnvMutex({ timeoutMs: 5_000 })
|
||||
expect(firstResult.acquired).toBe(true)
|
||||
|
||||
// Release after 50ms
|
||||
setTimeout(releaseEnvMutex, 50)
|
||||
|
||||
// Second acquire with 200ms timeout should succeed
|
||||
const result = await acquireEnvMutex({ timeoutMs: 200 })
|
||||
expect(result.acquired).toBe(true)
|
||||
|
||||
releaseEnvMutex()
|
||||
let acquiredSecond = false
|
||||
try {
|
||||
const result = await acquireEnvMutex({ timeoutMs: 200 })
|
||||
acquiredSecond = result.acquired
|
||||
expect(result.acquired).toBe(true)
|
||||
} finally {
|
||||
if (acquiredSecond) {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('acquireEnvMutex without timeout waits indefinitely (default behavior)', async () => {
|
||||
await acquireEnvMutex()
|
||||
const { acquireEnvMutex, releaseEnvMutex } = createEnvMutexForTesting()
|
||||
const firstResult = await acquireEnvMutex({ timeoutMs: 5_000 })
|
||||
expect(firstResult.acquired).toBe(true)
|
||||
|
||||
// Release after short delay
|
||||
setTimeout(releaseEnvMutex, 50)
|
||||
|
||||
// No timeout option - should wait and succeed
|
||||
const result = await acquireEnvMutex()
|
||||
expect(result.acquired).toBe(true)
|
||||
|
||||
releaseEnvMutex()
|
||||
let acquiredSecond = false
|
||||
try {
|
||||
const result = await acquireEnvMutex()
|
||||
acquiredSecond = result.acquired
|
||||
expect(result.acquired).toBe(true)
|
||||
} finally {
|
||||
if (acquiredSecond) {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('mutex remains functional after timeout', async () => {
|
||||
const { acquireEnvMutex, releaseEnvMutex } = createEnvMutexForTesting()
|
||||
// First acquire locks it
|
||||
await acquireEnvMutex()
|
||||
const firstResult = await acquireEnvMutex({ timeoutMs: 5_000 })
|
||||
expect(firstResult.acquired).toBe(true)
|
||||
|
||||
// Second acquire with timeout fails
|
||||
const result2 = await acquireEnvMutex({ timeoutMs: 50 })
|
||||
expect(result2.acquired).toBe(false)
|
||||
|
||||
// Release the first
|
||||
releaseEnvMutex()
|
||||
try {
|
||||
// Second acquire with timeout fails
|
||||
const result2 = await acquireEnvMutex({ timeoutMs: 50 })
|
||||
expect(result2.acquired).toBe(false)
|
||||
} finally {
|
||||
// Release the first
|
||||
releaseEnvMutex()
|
||||
}
|
||||
|
||||
// Third acquire should succeed (mutex not permanently locked)
|
||||
const result3 = await acquireEnvMutex({ timeoutMs: 100 })
|
||||
expect(result3.acquired).toBe(true)
|
||||
|
||||
releaseEnvMutex()
|
||||
try {
|
||||
expect(result3.acquired).toBe(true)
|
||||
} finally {
|
||||
if (result3.acquired) {
|
||||
releaseEnvMutex()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||
import { afterEach, describe, test, expect, beforeEach } from 'bun:test'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../src/test/sharedMutationLock.js'
|
||||
import {
|
||||
getToolSchemaCache,
|
||||
clearToolSchemaCache,
|
||||
@@ -6,10 +10,19 @@ import {
|
||||
} from '../../src/utils/toolSchemaCache.js'
|
||||
|
||||
describe('invalidateRemovedToolSchemas', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('tests/sdk/tool-schema-cache.test.ts')
|
||||
clearToolSchemaCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
clearToolSchemaCache()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
test('removes entries for tools not in retained set', () => {
|
||||
const cache = getToolSchemaCache()
|
||||
// Simulate cached tool schemas with different key formats
|
||||
@@ -81,4 +94,4 @@ describe('invalidateRemovedToolSchemas', () => {
|
||||
expect(cache.get('A')?.description).toBe('Tool A')
|
||||
expect(cache.get('B')?.description).toBe('Tool B')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user