Files
pi-file-claiming/tests/event-handlers.test.ts
2026-06-19 12:46:02 -04:00

721 lines
19 KiB
TypeScript

/**
* 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();