ded9671a82
- 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.
248 lines
7.5 KiB
TypeScript
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)
|
|
});
|
|
});
|