mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 20:58:14 -04:00
test: consolidate stub walls in compact and extractMemories.abort tests (#2329)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
spyOn,
|
||||
test,
|
||||
vi,
|
||||
} from 'bun:test'
|
||||
@@ -363,9 +364,23 @@ export type CompactMockOptions = {
|
||||
* mocks that the provider gate test specifically depends on; all others are
|
||||
* DEFENSIVE (prevent transitive import / side-effect / I/O failures).
|
||||
*/
|
||||
function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
async function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
mock.restore()
|
||||
|
||||
const [config, path, settings, model] = await Promise.all([
|
||||
import('../../utils/config.js'),
|
||||
import('../../utils/path.js'),
|
||||
import('../../utils/settings/settings.js'),
|
||||
import('../../utils/model/model.js'),
|
||||
])
|
||||
spyOn(config, 'getMemoryPath').mockReturnValue('/tmp/memory')
|
||||
if (options.getGlobalConfig) {
|
||||
spyOn(config, 'getGlobalConfig').mockImplementation(options.getGlobalConfig)
|
||||
}
|
||||
spyOn(path, 'expandPath').mockImplementation((p: string) => p)
|
||||
spyOn(settings, 'getInitialSettings').mockReturnValue({})
|
||||
spyOn(model, 'getCanonicalName').mockImplementation((m: string) => m)
|
||||
|
||||
// --- Provider gate control ---
|
||||
// The isAnthropicProvider() gate is exercised via environment variables
|
||||
// (e.g. CLAUDE_CODE_USE_OPENAI=1) instead of mock.module() on betas.ts.
|
||||
@@ -510,15 +525,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
groupMessagesByApiRound: mock((msgs: Message[]) => [msgs]),
|
||||
}))
|
||||
|
||||
// --- Config (DEFENSIVE) ---
|
||||
mock.module('../../utils/config.js', () => ({
|
||||
..._realConfigModule,
|
||||
getMemoryPath: mock(() => '/tmp/memory'),
|
||||
...(options.getGlobalConfig
|
||||
? { getGlobalConfig: options.getGlobalConfig }
|
||||
: {}),
|
||||
}))
|
||||
|
||||
// --- File state cache (DEFENSIVE) ---
|
||||
mock.module('../../utils/fileStateCache.js', () => ({
|
||||
cacheToObject: mock(() => ({})),
|
||||
@@ -555,12 +561,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
getPlanFilePath: mock(() => '/tmp/plan'),
|
||||
}))
|
||||
|
||||
// --- Path (DEFENSIVE) ---
|
||||
mock.module('../../utils/path.js', () => ({
|
||||
..._realPathModule,
|
||||
expandPath: mock((p: string) => p),
|
||||
}))
|
||||
|
||||
// --- Sleep (DEFENSIVE) ---
|
||||
mock.module('../../utils/sleep.js', () => ({
|
||||
sleep: mock(async () => {}),
|
||||
@@ -646,18 +646,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
get3PModelCapabilityOverride: mock(() => undefined),
|
||||
}))
|
||||
|
||||
// --- Settings (DEFENSIVE) ---
|
||||
mock.module('../../utils/settings/settings.js', () => ({
|
||||
..._realSettingsModule,
|
||||
getInitialSettings: mock(() => ({})),
|
||||
}))
|
||||
|
||||
// --- Model (DEFENSIVE) ---
|
||||
mock.module('../../utils/model/model.js', () => ({
|
||||
..._realModelModule,
|
||||
getCanonicalName: mock((m: string) => m),
|
||||
}))
|
||||
|
||||
return { runForkedAgent }
|
||||
}
|
||||
|
||||
@@ -682,7 +670,7 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) {
|
||||
* trigger hundreds of failed transitive resolution steps.
|
||||
*/
|
||||
async function importCompact(options: CompactMockOptions = {}) {
|
||||
const { runForkedAgent } = registerCommonCompactStubs(options)
|
||||
const { runForkedAgent } = await registerCommonCompactStubs(options)
|
||||
|
||||
// Dynamic import with cache-busting so each test gets fresh module state
|
||||
const nonce = `${Date.now()}-${Math.random()}`
|
||||
@@ -820,6 +808,143 @@ afterAll(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('compact media stripping', () => {
|
||||
test('strips top-level images and documents without mutating user messages', async () => {
|
||||
const { stripImagesFromMessages } = await import(
|
||||
`./compact.ts?test=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const message = {
|
||||
type: 'user',
|
||||
uuid: randomUUID(),
|
||||
timestamp: '2026-10-21T00:00:00.000Z',
|
||||
origin: { kind: 'human' },
|
||||
isMeta: true,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Summarize these attachments.' },
|
||||
{
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: 'aW1hZ2U=' },
|
||||
},
|
||||
{
|
||||
type: 'document',
|
||||
source: { type: 'text', media_type: 'text/plain', data: 'document' },
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Message
|
||||
const original = structuredClone(message)
|
||||
|
||||
expect(stripImagesFromMessages([message])).toEqual([
|
||||
{
|
||||
...original,
|
||||
message: {
|
||||
...original.message,
|
||||
content: [
|
||||
{ type: 'text', text: 'Summarize these attachments.' },
|
||||
{ type: 'text', text: '[image]' },
|
||||
{ type: 'text', text: '[document]' },
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(message).toEqual(original)
|
||||
expect(stripImagesFromMessages([])).toEqual([])
|
||||
|
||||
const stringMessage = userMessage('plain text')
|
||||
const textOnlyMessage = {
|
||||
...message,
|
||||
message: { role: 'user', content: [{ type: 'text', text: 'only text' }] },
|
||||
} satisfies Message
|
||||
const assistant = assistantMessage('assistant text')
|
||||
const unaffected = [stringMessage, textOnlyMessage, assistant]
|
||||
const stripped = stripImagesFromMessages(unaffected)
|
||||
expect(stripped).toEqual(unaffected)
|
||||
for (const [index, unchanged] of unaffected.entries()) {
|
||||
expect(stripped[index]).toBe(unchanged)
|
||||
}
|
||||
})
|
||||
|
||||
test('strips media inside tool results while preserving tool metadata and text', async () => {
|
||||
const { stripImagesFromMessages } = await import(
|
||||
`./compact.ts?test=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const message = {
|
||||
type: 'user',
|
||||
uuid: randomUUID(),
|
||||
timestamp: '2026-10-21T00:00:00.000Z',
|
||||
sourceToolUseID: 'toolu_media',
|
||||
toolUseResult: { status: 'failed' },
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'before the tool result' },
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'toolu_media',
|
||||
is_error: true,
|
||||
content: [
|
||||
{ type: 'text', text: 'tool text' },
|
||||
{
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: 'aW1hZ2U=' },
|
||||
},
|
||||
{
|
||||
type: 'document',
|
||||
source: { type: 'text', media_type: 'text/plain', data: 'document' },
|
||||
},
|
||||
{ type: 'text', text: 'trailing tool text' },
|
||||
],
|
||||
},
|
||||
{ type: 'tool_result', tool_use_id: 'toolu_string', content: 'string result' },
|
||||
{ type: 'tool_result', tool_use_id: 'toolu_empty', content: [] },
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'toolu_text',
|
||||
content: [{ type: 'text', text: 'text-only result' }],
|
||||
},
|
||||
{ type: 'text', text: 'after the tool result' },
|
||||
],
|
||||
},
|
||||
} satisfies Message
|
||||
const original = structuredClone(message)
|
||||
const stripped = stripImagesFromMessages([message])[0] as typeof message
|
||||
|
||||
expect(stripped).toEqual({
|
||||
...original,
|
||||
message: {
|
||||
...original.message,
|
||||
content: [
|
||||
original.message.content[0],
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'toolu_media',
|
||||
is_error: true,
|
||||
content: [
|
||||
{ type: 'text', text: 'tool text' },
|
||||
{ type: 'text', text: '[image]' },
|
||||
{ type: 'text', text: '[document]' },
|
||||
{ type: 'text', text: 'trailing tool text' },
|
||||
],
|
||||
},
|
||||
...original.message.content.slice(2),
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(message).toEqual(original)
|
||||
for (const index of [0, 2, 3, 4, 5]) {
|
||||
expect(stripped.message.content[index]).toBe(message.message.content[index])
|
||||
}
|
||||
|
||||
const unchanged = {
|
||||
...message,
|
||||
message: { ...message.message, content: message.message.content.slice(2) },
|
||||
}
|
||||
expect(stripImagesFromMessages([unchanged])[0]).toBe(unchanged)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compactConversation provider gate', () => {
|
||||
test('attributes cache-sharing timeout aborts to the compact fork', async () => {
|
||||
const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE
|
||||
|
||||
@@ -42,6 +42,64 @@ function createContext(messageUuids: string | string[]): any {
|
||||
}
|
||||
}
|
||||
|
||||
function registerExtractMemoriesStubs({
|
||||
runForkedAgent,
|
||||
debugLog,
|
||||
events,
|
||||
}: {
|
||||
runForkedAgent: ReturnType<typeof mock>
|
||||
debugLog: ReturnType<typeof mock<(message: string) => void>>
|
||||
events: string[]
|
||||
}) {
|
||||
mock.module('../../bootstrap/state.js', () => ({
|
||||
...realBootstrapState,
|
||||
getIsRemoteMode: () => false,
|
||||
}))
|
||||
mock.module('../../memdir/memoryScan.js', () => ({
|
||||
...realMemoryScan,
|
||||
formatMemoryManifest: () => 'No memories yet.',
|
||||
scanMemoryFiles: async () => [],
|
||||
}))
|
||||
mock.module('../../memdir/paths.js', () => ({
|
||||
...realPaths,
|
||||
getAutoMemPath: () => '/tmp/memory-extraction-test',
|
||||
isAutoMemoryEnabled: () => true,
|
||||
isAutoMemPath: () => false,
|
||||
}))
|
||||
mock.module('../../utils/governancePolicy.js', () => ({
|
||||
...realGovernance,
|
||||
isMemoryWriteApprovalRequired: () => false,
|
||||
}))
|
||||
mock.module('../../utils/debug.js', () => ({
|
||||
...realDebug,
|
||||
logForDebugging: debugLog,
|
||||
}))
|
||||
mock.module('../../utils/forkedAgent.js', () => ({
|
||||
...realForkedAgent,
|
||||
createCacheSafeParams: (context: any) => ({
|
||||
systemPrompt: context.systemPrompt,
|
||||
userContext: context.userContext,
|
||||
systemContext: context.systemContext,
|
||||
toolUseContext: context.toolUseContext,
|
||||
forkContextMessages: context.messages,
|
||||
}),
|
||||
runForkedAgent,
|
||||
}))
|
||||
mock.module('../analytics/growthbook.js', () => ({
|
||||
...realGrowthbook,
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: (
|
||||
key: string,
|
||||
defaultValue: unknown,
|
||||
) => (key === 'tengu_passport_quail' ? true : defaultValue),
|
||||
}))
|
||||
mock.module('../analytics/index.js', () => ({
|
||||
...realAnalytics,
|
||||
logEvent: (name: string) => {
|
||||
events.push(name)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
describe('extractMemories cancellation', () => {
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('extractMemories.abort.test.ts')
|
||||
@@ -94,53 +152,7 @@ describe('extractMemories cancellation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
mock.module('../../bootstrap/state.js', () => ({
|
||||
...realBootstrapState,
|
||||
getIsRemoteMode: () => false,
|
||||
}))
|
||||
mock.module('../../memdir/memoryScan.js', () => ({
|
||||
...realMemoryScan,
|
||||
formatMemoryManifest: () => 'No memories yet.',
|
||||
scanMemoryFiles: async () => [],
|
||||
}))
|
||||
mock.module('../../memdir/paths.js', () => ({
|
||||
...realPaths,
|
||||
getAutoMemPath: () => '/tmp/memory-extraction-test',
|
||||
isAutoMemoryEnabled: () => true,
|
||||
isAutoMemPath: () => false,
|
||||
}))
|
||||
mock.module('../../utils/governancePolicy.js', () => ({
|
||||
...realGovernance,
|
||||
isMemoryWriteApprovalRequired: () => false,
|
||||
}))
|
||||
mock.module('../../utils/debug.js', () => ({
|
||||
...realDebug,
|
||||
logForDebugging: debugLog,
|
||||
}))
|
||||
mock.module('../../utils/forkedAgent.js', () => ({
|
||||
...realForkedAgent,
|
||||
createCacheSafeParams: (context: any) => ({
|
||||
systemPrompt: context.systemPrompt,
|
||||
userContext: context.userContext,
|
||||
systemContext: context.systemContext,
|
||||
toolUseContext: context.toolUseContext,
|
||||
forkContextMessages: context.messages,
|
||||
}),
|
||||
runForkedAgent,
|
||||
}))
|
||||
mock.module('../analytics/growthbook.js', () => ({
|
||||
...realGrowthbook,
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: (
|
||||
key: string,
|
||||
defaultValue: unknown,
|
||||
) => (key === 'tengu_passport_quail' ? true : defaultValue),
|
||||
}))
|
||||
mock.module('../analytics/index.js', () => ({
|
||||
...realAnalytics,
|
||||
logEvent: (name: string) => {
|
||||
events.push(name)
|
||||
},
|
||||
}))
|
||||
registerExtractMemoriesStubs({ runForkedAgent, debugLog, events })
|
||||
|
||||
const { executeExtractMemories, initExtractMemories } = await import(
|
||||
`./extractMemories.js?ts=${Date.now()}-${Math.random()}`
|
||||
@@ -202,53 +214,7 @@ describe('extractMemories cancellation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
mock.module('../../bootstrap/state.js', () => ({
|
||||
...realBootstrapState,
|
||||
getIsRemoteMode: () => false,
|
||||
}))
|
||||
mock.module('../../memdir/memoryScan.js', () => ({
|
||||
...realMemoryScan,
|
||||
formatMemoryManifest: () => 'No memories yet.',
|
||||
scanMemoryFiles: async () => [],
|
||||
}))
|
||||
mock.module('../../memdir/paths.js', () => ({
|
||||
...realPaths,
|
||||
getAutoMemPath: () => '/tmp/memory-extraction-test',
|
||||
isAutoMemoryEnabled: () => true,
|
||||
isAutoMemPath: () => false,
|
||||
}))
|
||||
mock.module('../../utils/governancePolicy.js', () => ({
|
||||
...realGovernance,
|
||||
isMemoryWriteApprovalRequired: () => false,
|
||||
}))
|
||||
mock.module('../../utils/debug.js', () => ({
|
||||
...realDebug,
|
||||
logForDebugging: debugLog,
|
||||
}))
|
||||
mock.module('../../utils/forkedAgent.js', () => ({
|
||||
...realForkedAgent,
|
||||
createCacheSafeParams: (context: any) => ({
|
||||
systemPrompt: context.systemPrompt,
|
||||
userContext: context.userContext,
|
||||
systemContext: context.systemContext,
|
||||
toolUseContext: context.toolUseContext,
|
||||
forkContextMessages: context.messages,
|
||||
}),
|
||||
runForkedAgent,
|
||||
}))
|
||||
mock.module('../analytics/growthbook.js', () => ({
|
||||
...realGrowthbook,
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: (
|
||||
key: string,
|
||||
defaultValue: unknown,
|
||||
) => (key === 'tengu_passport_quail' ? true : defaultValue),
|
||||
}))
|
||||
mock.module('../analytics/index.js', () => ({
|
||||
...realAnalytics,
|
||||
logEvent: (name: string) => {
|
||||
events.push(name)
|
||||
},
|
||||
}))
|
||||
registerExtractMemoriesStubs({ runForkedAgent, debugLog, events })
|
||||
|
||||
const { executeExtractMemories, initExtractMemories } = await import(
|
||||
`./extractMemories.js?ts=${Date.now()}-${Math.random()}`
|
||||
@@ -307,53 +273,7 @@ describe('extractMemories cancellation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
mock.module('../../bootstrap/state.js', () => ({
|
||||
...realBootstrapState,
|
||||
getIsRemoteMode: () => false,
|
||||
}))
|
||||
mock.module('../../memdir/memoryScan.js', () => ({
|
||||
...realMemoryScan,
|
||||
formatMemoryManifest: () => 'No memories yet.',
|
||||
scanMemoryFiles: async () => [],
|
||||
}))
|
||||
mock.module('../../memdir/paths.js', () => ({
|
||||
...realPaths,
|
||||
getAutoMemPath: () => '/tmp/memory-extraction-test',
|
||||
isAutoMemoryEnabled: () => true,
|
||||
isAutoMemPath: () => false,
|
||||
}))
|
||||
mock.module('../../utils/governancePolicy.js', () => ({
|
||||
...realGovernance,
|
||||
isMemoryWriteApprovalRequired: () => false,
|
||||
}))
|
||||
mock.module('../../utils/debug.js', () => ({
|
||||
...realDebug,
|
||||
logForDebugging: debugLog,
|
||||
}))
|
||||
mock.module('../../utils/forkedAgent.js', () => ({
|
||||
...realForkedAgent,
|
||||
createCacheSafeParams: (context: any) => ({
|
||||
systemPrompt: context.systemPrompt,
|
||||
userContext: context.userContext,
|
||||
systemContext: context.systemContext,
|
||||
toolUseContext: context.toolUseContext,
|
||||
forkContextMessages: context.messages,
|
||||
}),
|
||||
runForkedAgent,
|
||||
}))
|
||||
mock.module('../analytics/growthbook.js', () => ({
|
||||
...realGrowthbook,
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: (
|
||||
key: string,
|
||||
defaultValue: unknown,
|
||||
) => (key === 'tengu_passport_quail' ? true : defaultValue),
|
||||
}))
|
||||
mock.module('../analytics/index.js', () => ({
|
||||
...realAnalytics,
|
||||
logEvent: (name: string) => {
|
||||
events.push(name)
|
||||
},
|
||||
}))
|
||||
registerExtractMemoriesStubs({ runForkedAgent, debugLog, events })
|
||||
|
||||
const { executeExtractMemories, initExtractMemories } = await import(
|
||||
`./extractMemories.js?ts=${Date.now()}-${Math.random()}`
|
||||
|
||||
Reference in New Issue
Block a user