Files
claude-mem/tests/happy-paths/session-cleanup.test.ts
T
Alex Newman ded9671a82 Refactor worker port handling and improve logging
- Replaced hardcoded migration port with dynamic port retrieval using `getWorkerPort()` in worker-cli.ts.
- Updated context generator to clarify error handling comments.
- Introduced timeout constants in ProcessManager for better maintainability.
- Configured SQLite settings using constants for mmap size and cache size in DatabaseManager.
- Added timeout constants for Git and NPM commands in BranchManager.
- Enhanced error logging in FormattingService and SearchManager to provide more context on failures.
- Removed deprecated silentDebug function and replaced its usage with logger.debug.
- Updated tests to use dynamic worker port retrieval instead of hardcoded values.
2025-12-11 14:49:47 -05:00

248 lines
7.5 KiB
TypeScript

/**
* Happy Path Test: Session Cleanup (SessionEnd)
*
* Tests that when a session ends, the worker marks it complete
* and performs necessary cleanup operations.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sessionScenario } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Session Cleanup (SessionEnd)', () => {
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
});
it('marks session complete and stops SDK agent', async () => {
// This tests the happy path:
// Session ends → Hook notifies worker → Worker marks session complete →
// SDK agent stopped → Resources cleaned up
// Setup: Mock successful response from worker
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ status: 'completed' })
});
// Execute: Send complete request (what cleanup-hook does)
const response = await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: sessionScenario.claudeSessionId,
reason: 'user_exit'
})
}
);
// Verify: Session marked complete
expect(response.ok).toBe(true);
const result = await response.json();
expect(result.status).toBe('completed');
// Verify: Correct data sent to worker
const fetchCall = (global.fetch as any).mock.calls[0];
const requestBody = JSON.parse(fetchCall[1].body);
expect(requestBody.claudeSessionId).toBe(sessionScenario.claudeSessionId);
expect(requestBody.reason).toBe('user_exit');
});
it('handles missing session ID gracefully', async () => {
// Setup: Mock error response
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: async () => ({ error: 'Missing claudeSessionId' })
});
// Execute: Send complete request without session ID
const response = await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason: 'user_exit'
})
}
);
// Verify: Returns error
expect(response.ok).toBe(false);
expect(response.status).toBe(400);
const error = await response.json();
expect(error.error).toContain('Missing claudeSessionId');
});
it('handles different session end reasons', async () => {
// Setup: Track all cleanup requests
const cleanupRequests: any[] = [];
global.fetch = vi.fn().mockImplementation(async (url, options) => {
const body = JSON.parse(options.body);
cleanupRequests.push(body);
return {
ok: true,
status: 200,
json: async () => ({ status: 'completed' })
};
});
// Test different end reasons
const reasons = [
'user_exit', // User explicitly ended session
'timeout', // Session timed out
'error', // Error occurred
'restart', // Session restarting
'clear' // User cleared context
];
// Execute: Send cleanup for each reason
for (const reason of reasons) {
await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: `session-${reason}`,
reason
})
}
);
}
// Verify: All cleanup requests processed
expect(cleanupRequests.length).toBe(5);
expect(cleanupRequests.map(r => r.reason)).toEqual(reasons);
});
it('completes multiple sessions independently', async () => {
// Setup: Track session completions
const completedSessions: string[] = [];
global.fetch = vi.fn().mockImplementation(async (url, options) => {
const body = JSON.parse(options.body);
completedSessions.push(body.claudeSessionId);
return {
ok: true,
status: 200,
json: async () => ({ status: 'completed' })
};
});
const sessions = [
'session-abc-123',
'session-def-456',
'session-ghi-789'
];
// Execute: Complete multiple sessions
for (const sessionId of sessions) {
await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: sessionId,
reason: 'user_exit'
})
}
);
}
// Verify: All sessions completed
expect(completedSessions).toEqual(sessions);
});
it('handles cleanup when session not found', async () => {
// Setup: Mock 404 response for non-existent session
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
json: async () => ({ error: 'Session not found' })
});
// Execute: Try to complete non-existent session
const response = await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: 'non-existent-session',
reason: 'user_exit'
})
}
);
// Verify: Returns 404 (graceful handling)
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
});
it('supports optional metadata in cleanup request', async () => {
// Setup: Mock worker response
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ status: 'completed' })
});
// Execute: Send cleanup with additional metadata
await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: sessionScenario.claudeSessionId,
reason: 'user_exit',
duration_seconds: 1800,
observations_count: 25,
project: 'claude-mem'
})
}
);
// Verify: Metadata included in request
const fetchCall = (global.fetch as any).mock.calls[0];
const requestBody = JSON.parse(fetchCall[1].body);
expect(requestBody.duration_seconds).toBe(1800);
expect(requestBody.observations_count).toBe(25);
expect(requestBody.project).toBe('claude-mem');
});
it('handles worker being down during cleanup', async () => {
// Setup: Mock worker unreachable
global.fetch = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
// Execute: Attempt to complete session
try {
await fetch(
`http://127.0.0.1:${WORKER_PORT}/api/sessions/complete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: sessionScenario.claudeSessionId,
reason: 'user_exit'
})
}
);
// Should throw, so fail if we get here
expect(true).toBe(false);
} catch (error: any) {
// Verify: Error indicates worker is down
expect(error.message).toContain('ECONNREFUSED');
}
// The hook should log this but not fail the session end
// (This graceful degradation would be tested in hook-specific tests)
});
});