fix(#338): Add session cleanup on terminal states

- Add removeSession and scheduleSessionCleanup methods to AgentSpawnerService
- Schedule session cleanup after completed/failed/killed transitions
- Default 30 second delay before cleanup to allow status queries
- Implement OnModuleDestroy to clean up pending timers
- Add forwardRef injection to avoid circular dependency
- Add comprehensive tests for cleanup functionality

Refs #338
This commit is contained in:
Jason Woltje
2026-02-05 18:47:14 -06:00
parent 8d57191a91
commit a42f88d64c
4 changed files with 347 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { AgentLifecycleService } from "./agent-lifecycle.service";
import { AgentSpawnerService } from "./agent-spawner.service";
import { ValkeyService } from "../valkey/valkey.service";
import type { AgentState } from "../valkey/types";
@@ -12,6 +13,9 @@ describe("AgentLifecycleService", () => {
publishEvent: ReturnType<typeof vi.fn>;
listAgents: ReturnType<typeof vi.fn>;
};
let mockSpawnerService: {
scheduleSessionCleanup: ReturnType<typeof vi.fn>;
};
const mockAgentId = "test-agent-123";
const mockTaskId = "test-task-456";
@@ -26,8 +30,15 @@ describe("AgentLifecycleService", () => {
listAgents: vi.fn(),
};
// Create service with mock
service = new AgentLifecycleService(mockValkeyService as unknown as ValkeyService);
mockSpawnerService = {
scheduleSessionCleanup: vi.fn(),
};
// Create service with mocks
service = new AgentLifecycleService(
mockValkeyService as unknown as ValkeyService,
mockSpawnerService as unknown as AgentSpawnerService
);
});
afterEach(() => {
@@ -612,4 +623,87 @@ describe("AgentLifecycleService", () => {
);
});
});
describe("session cleanup on terminal states", () => {
it("should schedule session cleanup when transitioning to completed", async () => {
const mockState: AgentState = {
agentId: mockAgentId,
status: "running",
taskId: mockTaskId,
startedAt: "2026-02-02T10:00:00Z",
};
mockValkeyService.getAgentState.mockResolvedValue(mockState);
mockValkeyService.updateAgentStatus.mockResolvedValue({
...mockState,
status: "completed",
completedAt: "2026-02-02T11:00:00Z",
});
await service.transitionToCompleted(mockAgentId);
expect(mockSpawnerService.scheduleSessionCleanup).toHaveBeenCalledWith(mockAgentId);
});
it("should schedule session cleanup when transitioning to failed", async () => {
const mockState: AgentState = {
agentId: mockAgentId,
status: "running",
taskId: mockTaskId,
startedAt: "2026-02-02T10:00:00Z",
};
const errorMessage = "Runtime error occurred";
mockValkeyService.getAgentState.mockResolvedValue(mockState);
mockValkeyService.updateAgentStatus.mockResolvedValue({
...mockState,
status: "failed",
error: errorMessage,
completedAt: "2026-02-02T11:00:00Z",
});
await service.transitionToFailed(mockAgentId, errorMessage);
expect(mockSpawnerService.scheduleSessionCleanup).toHaveBeenCalledWith(mockAgentId);
});
it("should schedule session cleanup when transitioning to killed", async () => {
const mockState: AgentState = {
agentId: mockAgentId,
status: "running",
taskId: mockTaskId,
startedAt: "2026-02-02T10:00:00Z",
};
mockValkeyService.getAgentState.mockResolvedValue(mockState);
mockValkeyService.updateAgentStatus.mockResolvedValue({
...mockState,
status: "killed",
completedAt: "2026-02-02T11:00:00Z",
});
await service.transitionToKilled(mockAgentId);
expect(mockSpawnerService.scheduleSessionCleanup).toHaveBeenCalledWith(mockAgentId);
});
it("should not schedule session cleanup when transitioning to running", async () => {
const mockState: AgentState = {
agentId: mockAgentId,
status: "spawning",
taskId: mockTaskId,
};
mockValkeyService.getAgentState.mockResolvedValue(mockState);
mockValkeyService.updateAgentStatus.mockResolvedValue({
...mockState,
status: "running",
startedAt: "2026-02-02T10:00:00Z",
});
await service.transitionToRunning(mockAgentId);
expect(mockSpawnerService.scheduleSessionCleanup).not.toHaveBeenCalled();
});
});
});