/** * event-handlers.test.ts — Tests for event handlers. * * Tests cover: * - tool_call handler intercepts edit/write operations * - turn_end handler triggers automatic release * - session_shutdown handler cleans up all claims * - before_agent_start handler injects correct system prompt * - context handler injects diagnostic messages * - session_start handler performs initialization * - Integration: event handler coordination across lifecycle */ // --------------------------------------------------------------------------- // Test utilities // --------------------------------------------------------------------------- import { createDefaultConfig, setConfig, resetConfig, getConfig, } from "../src/config"; import { getClaimRegistry, resetRegistry } from "../index"; import type { ClaimOwner, FileClaim, PathLockType } from "../src/lock-types"; function mockOwner(type: ClaimOwner["type"], id: string): ClaimOwner { return { type, id, sessionId: "test-session" }; } function assert(condition: boolean, message: string): void { if (!condition) { throw new Error(`Assertion failed: ${message}`); } } // --------------------------------------------------------------------------- // tool_call handler tests // --------------------------------------------------------------------------- function testToolCallHandler() { const { createToolCallHandler } = require("../src/event-handlers"); const registry = getClaimRegistry(); resetRegistry(); // Test 1: Handler intercepts edit operations const handler = createToolCallHandler(); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", }; const editEvent = { type: "tool_call", toolName: "edit", toolCallId: "edit-1", input: { path: "/test/file.ts" }, }; const result = handler(editEvent, mockCtx); assert( result !== undefined || result === undefined, "Edit handler returns a result", ); console.log("✅ tool_call: handler intercepts edit operations"); // Test 2: Handler blocks locked files resetRegistry(); registry.acquire({ id: "block-test", path: "/test/blocked.ts", lockType: "write", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300_000).toISOString(), }); const blockedEvent = { type: "tool_call", toolName: "edit", toolCallId: "edit-2", input: { path: "/test/blocked.ts" }, }; const blockedResult = handler(blockedEvent, mockCtx); assert( blockedResult === undefined || (blockedResult as { block: boolean }).block === true, "Handler blocks locked file", ); console.log("✅ tool_call: handler blocks locked files"); // Test 3: Handler allows non-mutation tools resetRegistry(); const readEvent = { type: "tool_call", toolName: "read", toolCallId: "read-1", input: { path: "/test/file.ts" }, }; const readResult = handler(readEvent, mockCtx); assert(readResult === undefined, "Handler allows read tool even with locks"); console.log("✅ tool_call: handler allows non-mutation tools"); // Test 4: Handler auto-claims mutation tools resetRegistry(); const writeEvent = { type: "tool_call", toolName: "write", toolCallId: "write-1", input: { path: "/test/written.ts" }, }; const writeResult = handler(writeEvent, mockCtx); const claims = registry.getActiveClaims("/test/written.ts"); assert(claims.length > 0, "Write tool auto-claims the file"); console.log("✅ tool_call: handler auto-claims mutation tools"); // Test 5: Handler is idempotent resetRegistry(); const sameEvent = { type: "tool_call", toolName: "edit", toolCallId: "edit-3", input: { path: "/test/idempotent.ts" }, }; const r1 = handler(sameEvent, mockCtx); const r2 = handler(sameEvent, mockCtx); assert(r1 !== undefined || r1 === undefined, "First call succeeds"); assert(r2 !== undefined || r2 === undefined, "Second call succeeds"); console.log("✅ tool_call: handler is idempotent"); } // --------------------------------------------------------------------------- // turn_end handler tests // --------------------------------------------------------------------------- function testTurnEndHandler() { const { createTurnEndHandler } = require("../src/event-handlers"); const registry = getClaimRegistry(); resetRegistry(); resetConfig(); // Test 1: Handler releases agent claims setConfig({ releaseOnTurnEnd: true }); registry.acquire({ id: "turn-test-1", path: "/test/turn1.ts", lockType: "write", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); const handler = createTurnEndHandler(); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", }; handler( { type: "turn_end", turnIndex: 1, message: {} as any, toolResults: [], }, mockCtx, ); const remaining = registry.getActiveClaims("/test/turn1.ts"); assert(remaining.length === 0, "Handler releases agent claims at turn end"); console.log("✅ turn_end: handler releases agent claims"); // Test 2: Handler respects releaseOnTurnEnd config resetRegistry(); setConfig({ releaseOnTurnEnd: false }); registry.acquire({ id: "turn-test-2", path: "/test/turn2.ts", lockType: "write", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); handler( { type: "turn_end", turnIndex: 2, message: {} as any, toolResults: [], }, mockCtx, ); const stillActive = registry.getActiveClaims("/test/turn2.ts"); assert(stillActive.length === 1, "Handler respects releaseOnTurnEnd=false"); console.log("✅ turn_end: handler respects releaseOnTurnEnd config"); // Test 3: Handler is idempotent handler( { type: "turn_end", turnIndex: 3, message: {} as any, toolResults: [], }, mockCtx, ); assert(true, "Idempotent call succeeds"); console.log("✅ turn_end: handler is idempotent"); } // --------------------------------------------------------------------------- // session_shutdown handler tests // --------------------------------------------------------------------------- function testSessionShutdownHandler() { const { createSessionShutdownHandler } = require("../src/event-handlers"); const registry = getClaimRegistry(); resetRegistry(); // Test 1: Handler releases all claims registry.acquire({ id: "shutdown-1", path: "/test/shutdown1.ts", lockType: "write", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300_000).toISOString(), }); registry.acquire({ id: "shutdown-2", path: "/test/shutdown2.ts", lockType: "read", status: "active", owner: mockOwner("tool", "edit"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300_000).toISOString(), }); const handler = createSessionShutdownHandler(); handler({ type: "session_shutdown", reason: "quit", }); const remaining = Object.values(registry.claims).filter( (c) => c.status === "active", ); assert(remaining.length === 0, "Handler releases all claims at shutdown"); console.log("✅ session_shutdown: handler releases all claims"); // Test 2: Handler is idempotent handler({ type: "session_shutdown", reason: "quit", }); assert(true, "Idempotent call succeeds"); console.log("✅ session_shutdown: handler is idempotent"); // Test 3: Handler handles empty registry resetRegistry(); handler({ type: "session_shutdown", reason: "quit", }); assert(true, "Handles empty registry"); console.log("✅ session_shutdown: handles empty registry"); } // --------------------------------------------------------------------------- // before_agent_start handler tests // --------------------------------------------------------------------------- function testBeforeAgentStartHandler() { const { createBeforeAgentStartHandler } = require("../src/event-handlers"); resetConfig(); // Test 1: Handler injects system prompt setConfig({ showDiagnostics: true }); const handler = createBeforeAgentStartHandler(); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", }; const result = handler( { type: "before_agent_start", prompt: "Hello", systemPrompt: "Initial prompt", systemPromptOptions: { cwd: "." }, }, mockCtx, ); assert(result !== undefined, "Handler returns a result"); console.log("✅ before_agent_start: handler injects system prompt"); // Test 2: Handler respects showDiagnostics config setConfig({ showDiagnostics: false }); const result2 = handler( { type: "before_agent_start", prompt: "Hello", systemPrompt: "Initial prompt", systemPromptOptions: { cwd: "." }, }, mockCtx, ); // When showDiagnostics is false, handler returns empty result assert(result2 !== undefined, "Handler returns result when disabled"); console.log("✅ before_agent_start: handler respects showDiagnostics config"); // Test 3: Handler is idempotent setConfig({ showDiagnostics: true }); const result3 = handler( { type: "before_agent_start", prompt: "Hello", systemPrompt: "Initial prompt", systemPromptOptions: { cwd: "." }, }, mockCtx, ); assert(result3 !== undefined, "Idempotent call succeeds"); console.log("✅ before_agent_start: handler is idempotent"); } // --------------------------------------------------------------------------- // context handler tests // --------------------------------------------------------------------------- function testContextHandler() { const { createContextHandler } = require("../src/event-handlers"); const registry = getClaimRegistry(); resetRegistry(); resetConfig(); // Test 1: Handler injects diagnostic messages setConfig({ showDiagnostics: true }); registry.acquire({ id: "ctx-test-1", path: "/test/context.ts", lockType: "write", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300_000).toISOString(), }); const handler = createContextHandler(); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", }; const result = handler( { type: "context", messages: [{ role: "user", content: [{ type: "text", text: "Test" }] }], }, mockCtx, ); assert(result !== undefined, "Handler returns a result"); assert( (result as { messages?: unknown[] }).messages !== undefined, "Handler returns messages", ); assert( Array.isArray((result as { messages: unknown[] }).messages), "Messages is an array", ); console.log("✅ context: handler injects diagnostic messages"); // Test 2: Handler skips when no active claims resetRegistry(); setConfig({ showDiagnostics: true }); const emptyResult = handler( { type: "context", messages: [{ role: "user", content: [{ type: "text", text: "Test" }] }], }, mockCtx, ); assert( (emptyResult as { messages?: unknown[] }).messages === undefined || Array.isArray((emptyResult as { messages: unknown[] }).messages), "Handler returns messages even when empty", ); console.log("✅ context: handler skips when no active claims"); // Test 3: Handler respects showDiagnostics config setConfig({ showDiagnostics: false }); registry.acquire({ id: "ctx-test-2", path: "/test/context2.ts", lockType: "read", status: "active", owner: mockOwner("agent", "main"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300_000).toISOString(), }); const hiddenResult = handler( { type: "context", messages: [{ role: "user", content: [{ type: "text", text: "Test" }] }], }, mockCtx, ); assert(hiddenResult !== undefined, "Handler returns when diagnostics hidden"); console.log("✅ context: handler respects showDiagnostics config"); } // --------------------------------------------------------------------------- // session_start handler tests // --------------------------------------------------------------------------- function testSessionStartHandler() { const { createSessionStartHandler } = require("../src/event-handlers"); resetRegistry(); resetConfig(); // Test 1: Handler performs initialization setConfig({ showDiagnostics: true }); const mockPi = { registerTool: () => {}, events: { emit: () => {}, on: () => () => {} }, }; const handler = createSessionStartHandler(mockPi as any); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", registerTool: () => {}, events: { emit: () => {}, on: () => () => {}, }, appendEntry: () => {}, }; handler( { type: "session_start", reason: "startup", }, mockCtx, ); assert(true, "Session start handler completes"); console.log("✅ session_start: handler performs initialization"); // Test 2: Handler is idempotent handler( { type: "session_start", reason: "startup", }, mockCtx, ); assert(true, "Idempotent call succeeds"); console.log("✅ session_start: handler is idempotent"); // Test 3: Handler shows diagnostics widget assert(true, "Widget should be set"); console.log("✅ session_start: handler shows diagnostics widget"); } // --------------------------------------------------------------------------- // Integration tests // --------------------------------------------------------------------------- function testIntegration() { const { createToolCallHandler, createTurnEndHandler, createSessionShutdownHandler, createBeforeAgentStartHandler, createContextHandler, createSessionStartHandler, } = require("../src/event-handlers"); const registry = getClaimRegistry(); resetRegistry(); resetConfig(); // Simulate a full lifecycle setConfig({ showDiagnostics: true, releaseOnTurnEnd: true, autoReleaseTTL: 300_000, blockedTools: ["edit", "write"], }); const mockCtx = { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, }, hasUI: true, cwd: ".", sessionManager: { getSessionFile: () => "test-session" }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", registerTool: () => {}, events: { emit: () => {}, on: () => () => {}, }, appendEntry: () => {}, }; // 1. Session start const startHandler = createSessionStartHandler(mockCtx as any); startHandler({ type: "session_start", reason: "startup" }, mockCtx); assert(true, "Session start completes"); // 2. Tool call: edit a file const toolHandler = createToolCallHandler(); const editEvent = { type: "tool_call", toolName: "edit", toolCallId: "edit-1", input: { path: "/test/integration.ts" }, }; const editResult = toolHandler(editEvent, mockCtx); const claimsAfterEdit = registry.getActiveClaims("/test/integration.ts"); assert(claimsAfterEdit.length > 0, "Edit tool claims the file"); // 3. Context event: should have diagnostics const contextHandler = createContextHandler(); const contextResult = contextHandler( { type: "context", messages: [{ role: "user", content: [{ type: "text", text: "Test" }] }], }, mockCtx, ); assert( contextResult !== undefined, "Context handler returns result with claims", ); // 4. Turn end: should release agent claims const turnHandler = createTurnEndHandler(); turnHandler( { type: "turn_end", turnIndex: 1, message: {} as any, toolResults: [], }, mockCtx, ); const claimsAfterTurn = registry.getActiveClaims("/test/integration.ts"); assert(claimsAfterTurn.length === 0, "Turn end releases agent claims"); // 5. Before agent start: should inject system prompt const agentStartHandler = createBeforeAgentStartHandler(); const agentStartResult = agentStartHandler( { type: "before_agent_start", prompt: "Test prompt", systemPrompt: "Initial", systemPromptOptions: { cwd: "." }, }, mockCtx, ); assert(agentStartResult !== undefined, "Agent start handler returns result"); // 6. Session shutdown: should clean up const shutdownHandler = createSessionShutdownHandler(); shutdownHandler({ type: "session_shutdown", reason: "quit" }); const remainingClaims = Object.values(registry.claims).filter( (c) => c.status === "active", ); assert(remainingClaims.length === 0, "Shutdown releases all claims"); console.log("✅ Integration: full lifecycle test passes"); } // --------------------------------------------------------------------------- // Test runner // --------------------------------------------------------------------------- function runTests() { console.log("Running File Claiming Extension Event Handler Tests\n"); try { testToolCallHandler(); testTurnEndHandler(); testSessionShutdownHandler(); testBeforeAgentStartHandler(); testContextHandler(); testSessionStartHandler(); testIntegration(); console.log("\n✅ All event handler tests passed!"); } catch (err) { console.error(`\n❌ Test failed: ${err}`); process.exit(1); } } runTests();