Files
Codeman/test/session-cleanup.test.ts
T
arkonandClaude Opus 4.5 619f2a3403 fix: clean up test case directories after each test instead of at end
Moves case directory cleanup from afterAll to afterEach in all test
files that create cases. Previously, if the test suite was interrupted
or afterAll timed out, all case directories were left behind. Now each
test cleans up immediately after itself.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 22:54:26 +01:00

287 lines
9.8 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
import { WebServer } from '../src/web/server.js';
import { existsSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3120;
const CASES_DIR = join(homedir(), 'claudeman-cases');
/**
* Session Cleanup Tests
*
* Tests for automatic session cleanup and resource management
*/
describe('Session Cleanup', () => {
let server: WebServer;
let baseUrl: string;
const createdCases: string[] = [];
const createdSessions: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
});
afterEach(() => {
// Clean up cases created during this test
while (createdCases.length > 0) {
const caseName = createdCases.pop()!;
const casePath = join(CASES_DIR, caseName);
if (existsSync(casePath)) {
rmSync(casePath, { recursive: true, force: true });
}
}
});
afterAll(async () => {
// Cleanup all sessions
for (const sessionId of createdSessions) {
try {
await fetch(`${baseUrl}/api/sessions/${sessionId}`, { method: 'DELETE' });
} catch {}
}
await server.stop();
}, 60000);
describe('Session Deletion', () => {
it('should properly stop and cleanup interactive session', async () => {
const caseName = `cleanup-test-${Date.now()}`;
createdCases.push(caseName);
// Create and start interactive session
const quickStartRes = await fetch(`${baseUrl}/api/quick-start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName }),
});
const quickStartData = await quickStartRes.json();
expect(quickStartData.success).toBe(true);
// Delete the session
const deleteRes = await fetch(`${baseUrl}/api/sessions/${quickStartData.sessionId}`, {
method: 'DELETE',
});
const deleteData = await deleteRes.json();
expect(deleteData.success).toBe(true);
// Verify session is gone
const getRes = await fetch(`${baseUrl}/api/sessions/${quickStartData.sessionId}`);
const getData = await getRes.json();
expect(getData.error).toBe('Session not found');
});
it('should cleanup multiple sessions when deleted', async () => {
const sessionIds: string[] = [];
// Create multiple sessions
for (let i = 0; i < 3; i++) {
const caseName = `multi-cleanup-${Date.now()}-${i}`;
createdCases.push(caseName);
const res = await fetch(`${baseUrl}/api/quick-start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName }),
});
const data = await res.json();
expect(data.success).toBe(true);
sessionIds.push(data.sessionId);
}
// Delete all sessions
for (const id of sessionIds) {
const deleteRes = await fetch(`${baseUrl}/api/sessions/${id}`, {
method: 'DELETE',
});
const deleteData = await deleteRes.json();
expect(deleteData.success).toBe(true);
}
// Verify all sessions are gone
const listRes = await fetch(`${baseUrl}/api/sessions`);
const sessions = await listRes.json();
for (const id of sessionIds) {
expect(sessions.find((s: any) => s.id === id)).toBeUndefined();
}
}, 60000); // Extended timeout for multi-session test
});
describe('Respawn Controller Cleanup', () => {
it('should cleanup respawn controller when session is deleted', async () => {
const caseName = `respawn-cleanup-${Date.now()}`;
createdCases.push(caseName);
// Create session
const sessionRes = await fetch(`${baseUrl}/api/sessions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workingDir: '/tmp' }),
});
const sessionData = await sessionRes.json();
expect(sessionData.success).toBe(true);
// Start interactive with respawn
const interactiveRes = await fetch(`${baseUrl}/api/sessions/${sessionData.session.id}/interactive-respawn`, {
method: 'POST',
});
const interactiveData = await interactiveRes.json();
expect(interactiveData.success).toBe(true);
// Verify respawn is running
const respawnRes = await fetch(`${baseUrl}/api/sessions/${sessionData.session.id}/respawn`);
const respawnData = await respawnRes.json();
expect(respawnData.enabled).toBe(true);
// Delete session
await fetch(`${baseUrl}/api/sessions/${sessionData.session.id}`, {
method: 'DELETE',
});
// Wait for cleanup to complete (exit event handler)
await new Promise(resolve => setTimeout(resolve, 500));
// Verify respawn controller is cleaned up (enabled: false when controller doesn't exist)
const respawnAfterRes = await fetch(`${baseUrl}/api/sessions/${sessionData.session.id}/respawn`);
const respawnAfterData = await respawnAfterRes.json();
// The respawn endpoint returns enabled: false when there's no controller
expect(respawnAfterData.enabled).toBe(false);
});
});
describe('Scheduled Run Cleanup', () => {
it('should properly stop scheduled run', async () => {
// Create a scheduled run
const createRes = await fetch(`${baseUrl}/api/scheduled`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: 'test cleanup',
workingDir: '/tmp',
durationMinutes: 1,
}),
});
const createData = await createRes.json();
expect(createData.success).toBe(true);
// Stop the scheduled run
const stopRes = await fetch(`${baseUrl}/api/scheduled/${createData.run.id}`, {
method: 'DELETE',
});
const stopData = await stopRes.json();
expect(stopData.success).toBe(true);
// Verify status is stopped
const getRes = await fetch(`${baseUrl}/api/scheduled/${createData.run.id}`);
const getData = await getRes.json();
expect(getData.status).toBe('stopped');
});
});
});
describe('Resource Management', () => {
let server: WebServer;
let baseUrl: string;
const createdCases: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
});
afterAll(async () => {
for (const caseName of createdCases) {
const casePath = join(CASES_DIR, caseName);
if (existsSync(casePath)) {
rmSync(casePath, { recursive: true, force: true });
}
}
await server.stop();
}, 60000);
it('should handle rapid session creation and deletion', async () => {
// Get initial session count (may have restored sessions from other tests)
const initialRes = await fetch(`${baseUrl}/api/sessions`);
const initialSessions = await initialRes.json();
const initialCount = initialSessions.length;
const iterations = 5;
const createdSessionIds: string[] = [];
for (let i = 0; i < iterations; i++) {
const caseName = `rapid-${Date.now()}-${i}`;
createdCases.push(caseName);
// Create
const createRes = await fetch(`${baseUrl}/api/quick-start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName }),
});
const createData = await createRes.json();
expect(createData.success).toBe(true);
createdSessionIds.push(createData.sessionId);
// Delete immediately
const deleteRes = await fetch(`${baseUrl}/api/sessions/${createData.sessionId}`, {
method: 'DELETE',
});
const deleteData = await deleteRes.json();
expect(deleteData.success).toBe(true);
// Small delay to allow async cleanup to complete
await new Promise(resolve => setTimeout(resolve, 100));
}
// Wait a bit more for all cleanup to complete
await new Promise(resolve => setTimeout(resolve, 500));
// Verify created sessions were deleted (account for restored sessions from other tests)
const listRes = await fetch(`${baseUrl}/api/sessions`);
const sessions = await listRes.json();
// None of the sessions we created should still exist
for (const sessionId of createdSessionIds) {
expect(sessions.find((s: { id: string }) => s.id === sessionId)).toBeUndefined();
}
// Session count should be back to initial (or less if some restored sessions were cleaned up)
expect(sessions.length).toBeLessThanOrEqual(initialCount);
});
it('should clear terminal buffer after session stop', async () => {
const caseName = `buffer-clear-${Date.now()}`;
createdCases.push(caseName);
// Create session
const createRes = await fetch(`${baseUrl}/api/quick-start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName }),
});
const createData = await createRes.json();
expect(createData.success).toBe(true);
// Wait for some terminal output - Claude startup time can vary
await new Promise(resolve => setTimeout(resolve, 2000));
// Get terminal buffer before stop - may or may not have content depending on timing
const terminalRes = await fetch(`${baseUrl}/api/sessions/${createData.sessionId}/terminal`);
const terminalData = await terminalRes.json();
// Just verify the property exists
expect(terminalData.terminalBuffer).toBeDefined();
// Delete session
await fetch(`${baseUrl}/api/sessions/${createData.sessionId}`, {
method: 'DELETE',
});
// Session should be gone
const afterRes = await fetch(`${baseUrl}/api/sessions/${createData.sessionId}/terminal`);
const afterData = await afterRes.json();
expect(afterData.error).toBe('Session not found');
});
});