mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
Top-level mock.module() in context-reinjection-guard.test.ts permanently stubbed getProjectName() to 'test-project' for the entire Bun worker process, causing tests in other files to receive the wrong value. Removed the unnecessary mock (session-init tests don't assert on project name), added bunfig.toml smol=true for worker isolation, and added a regression test. Generated by Claude Code Vibe coded by ousamabenyounes Co-Authored-By: Claude <noreply@anthropic.com>
307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
/**
|
|
* Tests for Context Re-Injection Guard (#1079)
|
|
*
|
|
* Validates:
|
|
* - session-init handler skips SDK agent init when contextInjected=true
|
|
* - session-init handler proceeds with SDK agent init when contextInjected=false
|
|
* - SessionManager.getSession returns undefined for uninitialized sessions
|
|
* - SessionManager.getSession returns session after initialization
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from 'bun:test';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
// Mock modules that cause import chain issues - MUST be before handler imports
|
|
// paths.ts calls SettingsDefaultsManager.get() at module load time
|
|
mock.module('../../src/shared/SettingsDefaultsManager.js', () => ({
|
|
SettingsDefaultsManager: {
|
|
get: (key: string) => {
|
|
if (key === 'CLAUDE_MEM_DATA_DIR') return join(homedir(), '.claude-mem');
|
|
return '';
|
|
},
|
|
getInt: () => 0,
|
|
loadFromFile: () => ({ CLAUDE_MEM_EXCLUDED_PROJECTS: [] }),
|
|
},
|
|
}));
|
|
|
|
mock.module('../../src/shared/worker-utils.js', () => ({
|
|
ensureWorkerRunning: () => Promise.resolve(true),
|
|
getWorkerPort: () => 37777,
|
|
}));
|
|
|
|
mock.module('../../src/utils/project-filter.js', () => ({
|
|
isProjectExcluded: () => false,
|
|
}));
|
|
|
|
// Now import after mocks
|
|
import { logger } from '../../src/utils/logger.js';
|
|
|
|
// Suppress logger output during tests
|
|
let loggerSpies: ReturnType<typeof spyOn>[] = [];
|
|
|
|
beforeEach(() => {
|
|
loggerSpies = [
|
|
spyOn(logger, 'info').mockImplementation(() => {}),
|
|
spyOn(logger, 'debug').mockImplementation(() => {}),
|
|
spyOn(logger, 'warn').mockImplementation(() => {}),
|
|
spyOn(logger, 'error').mockImplementation(() => {}),
|
|
spyOn(logger, 'failure').mockImplementation(() => {}),
|
|
];
|
|
});
|
|
|
|
afterEach(() => {
|
|
loggerSpies.forEach(spy => spy.mockRestore());
|
|
});
|
|
|
|
describe('Context Re-Injection Guard (#1079)', () => {
|
|
describe('session-init handler - contextInjected flag behavior', () => {
|
|
it('should skip SDK agent init when contextInjected is true', async () => {
|
|
const fetchedUrls: string[] = [];
|
|
|
|
const mockFetch = mock((url: string | URL | Request) => {
|
|
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
fetchedUrls.push(urlStr);
|
|
|
|
if (urlStr.includes('/api/sessions/init')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
sessionDbId: 42,
|
|
promptNumber: 2,
|
|
skipped: false,
|
|
contextInjected: true // SDK agent already running
|
|
})
|
|
});
|
|
}
|
|
|
|
// The /sessions/42/init call — should NOT be reached
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ status: 'initialized' })
|
|
});
|
|
});
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = mockFetch as any;
|
|
|
|
try {
|
|
const { sessionInitHandler } = await import('../../src/cli/handlers/session-init.js');
|
|
|
|
const result = await sessionInitHandler.execute({
|
|
sessionId: 'test-session-123',
|
|
cwd: '/test/project',
|
|
prompt: 'second prompt in this session',
|
|
platform: 'claude-code',
|
|
});
|
|
|
|
// Should return success without making the second /sessions/42/init call
|
|
expect(result.continue).toBe(true);
|
|
expect(result.suppressOutput).toBe(true);
|
|
|
|
// Only the /api/sessions/init call should have been made
|
|
const apiInitCalls = fetchedUrls.filter(u => u.includes('/api/sessions/init'));
|
|
const sdkInitCalls = fetchedUrls.filter(u => u.includes('/sessions/42/init'));
|
|
|
|
expect(apiInitCalls.length).toBe(1);
|
|
expect(sdkInitCalls.length).toBe(0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it('should proceed with SDK agent init when contextInjected is false', async () => {
|
|
const fetchedUrls: string[] = [];
|
|
|
|
const mockFetch = mock((url: string | URL | Request) => {
|
|
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
fetchedUrls.push(urlStr);
|
|
|
|
if (urlStr.includes('/api/sessions/init')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
sessionDbId: 42,
|
|
promptNumber: 1,
|
|
skipped: false,
|
|
contextInjected: false // First prompt — SDK agent not yet started
|
|
})
|
|
});
|
|
}
|
|
|
|
// The /sessions/42/init call — SHOULD be reached
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ status: 'initialized' })
|
|
});
|
|
});
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = mockFetch as any;
|
|
|
|
try {
|
|
const { sessionInitHandler } = await import('../../src/cli/handlers/session-init.js');
|
|
|
|
const result = await sessionInitHandler.execute({
|
|
sessionId: 'test-session-456',
|
|
cwd: '/test/project',
|
|
prompt: 'first prompt in session',
|
|
platform: 'claude-code',
|
|
});
|
|
|
|
expect(result.continue).toBe(true);
|
|
expect(result.suppressOutput).toBe(true);
|
|
|
|
// Both calls should have been made
|
|
const apiInitCalls = fetchedUrls.filter(u => u.includes('/api/sessions/init'));
|
|
const sdkInitCalls = fetchedUrls.filter(u => u.includes('/sessions/42/init'));
|
|
|
|
expect(apiInitCalls.length).toBe(1);
|
|
expect(sdkInitCalls.length).toBe(1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it('should proceed with SDK agent init when contextInjected is undefined (backward compat)', async () => {
|
|
const fetchedUrls: string[] = [];
|
|
|
|
const mockFetch = mock((url: string | URL | Request) => {
|
|
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
fetchedUrls.push(urlStr);
|
|
|
|
if (urlStr.includes('/api/sessions/init')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
sessionDbId: 42,
|
|
promptNumber: 1,
|
|
skipped: false
|
|
// contextInjected not present (older worker version)
|
|
})
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ status: 'initialized' })
|
|
});
|
|
});
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = mockFetch as any;
|
|
|
|
try {
|
|
const { sessionInitHandler } = await import('../../src/cli/handlers/session-init.js');
|
|
|
|
const result = await sessionInitHandler.execute({
|
|
sessionId: 'test-session-789',
|
|
cwd: '/test/project',
|
|
prompt: 'test prompt',
|
|
platform: 'claude-code',
|
|
});
|
|
|
|
expect(result.continue).toBe(true);
|
|
|
|
// When contextInjected is undefined/missing, should still make the SDK init call
|
|
const sdkInitCalls = fetchedUrls.filter(u => u.includes('/sessions/42/init'));
|
|
expect(sdkInitCalls.length).toBe(1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('SessionManager contextInjected logic', () => {
|
|
it('should return undefined for getSession when no active session exists', async () => {
|
|
const { SessionManager } = await import('../../src/services/worker/SessionManager.js');
|
|
|
|
const mockDbManager = {
|
|
getSessionById: () => ({
|
|
id: 1,
|
|
content_session_id: 'test-session',
|
|
project: 'test',
|
|
user_prompt: 'test prompt',
|
|
memory_session_id: null,
|
|
status: 'active',
|
|
started_at: new Date().toISOString(),
|
|
completed_at: null,
|
|
}),
|
|
getSessionStore: () => ({ db: {} }),
|
|
} as any;
|
|
|
|
const sessionManager = new SessionManager(mockDbManager);
|
|
|
|
// Session 42 has not been initialized in memory
|
|
const session = sessionManager.getSession(42);
|
|
expect(session).toBeUndefined();
|
|
});
|
|
|
|
it('should return active session after initializeSession is called', async () => {
|
|
const { SessionManager } = await import('../../src/services/worker/SessionManager.js');
|
|
|
|
const mockDbManager = {
|
|
getSessionById: () => ({
|
|
id: 42,
|
|
content_session_id: 'test-session',
|
|
project: 'test',
|
|
user_prompt: 'test prompt',
|
|
memory_session_id: null,
|
|
status: 'active',
|
|
started_at: new Date().toISOString(),
|
|
completed_at: null,
|
|
}),
|
|
getSessionStore: () => ({
|
|
db: {},
|
|
clearMemorySessionId: () => {},
|
|
}),
|
|
} as any;
|
|
|
|
const sessionManager = new SessionManager(mockDbManager);
|
|
|
|
// Initialize session (simulates first SDK agent init)
|
|
sessionManager.initializeSession(42, 'first prompt', 1);
|
|
|
|
// Now getSession should return the active session
|
|
const session = sessionManager.getSession(42);
|
|
expect(session).toBeDefined();
|
|
expect(session!.contentSessionId).toBe('test-session');
|
|
});
|
|
|
|
it('should return contextInjected=true pattern for subsequent prompts', async () => {
|
|
const { SessionManager } = await import('../../src/services/worker/SessionManager.js');
|
|
|
|
const mockDbManager = {
|
|
getSessionById: () => ({
|
|
id: 42,
|
|
content_session_id: 'test-session',
|
|
project: 'test',
|
|
user_prompt: 'test prompt',
|
|
memory_session_id: 'sdk-session-abc',
|
|
status: 'active',
|
|
started_at: new Date().toISOString(),
|
|
completed_at: null,
|
|
}),
|
|
getSessionStore: () => ({
|
|
db: {},
|
|
clearMemorySessionId: () => {},
|
|
}),
|
|
} as any;
|
|
|
|
const sessionManager = new SessionManager(mockDbManager);
|
|
|
|
// Before initialization: contextInjected would be false
|
|
expect(sessionManager.getSession(42)).toBeUndefined();
|
|
|
|
// After initialization: contextInjected would be true
|
|
sessionManager.initializeSession(42, 'first prompt', 1);
|
|
expect(sessionManager.getSession(42)).toBeDefined();
|
|
|
|
// Second call to initializeSession returns existing session (idempotent)
|
|
const session2 = sessionManager.initializeSession(42, 'second prompt', 2);
|
|
expect(session2.contentSessionId).toBe('test-session');
|
|
expect(session2.userPrompt).toBe('second prompt');
|
|
expect(session2.lastPromptNumber).toBe(2);
|
|
});
|
|
});
|
|
});
|