1000 lines
29 KiB
TypeScript
1000 lines
29 KiB
TypeScript
/**
|
|
* index.test.ts — Tests for the File Claiming extension LLM integration.
|
|
*
|
|
* Tests cover:
|
|
* - System prompt injection
|
|
* - Diagnostic message formatting and delivery
|
|
* - Tool registration
|
|
* - Notification system for various lock events
|
|
* - User interaction components
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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" };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// System prompt injection tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testSystemPromptInjection() {
|
|
const {
|
|
injectLockClaimingIntoPrompt,
|
|
buildLockClaimingInstructions,
|
|
buildLockClaimingGuidelines,
|
|
buildLockClaimingToolSnippets,
|
|
} = require("../src/system-prompt");
|
|
|
|
// Test 1: Instructions are injected
|
|
const instructions = buildLockClaimingInstructions();
|
|
assert(
|
|
instructions.includes("<file_claiming>"),
|
|
"Instructions include file_claiming tags",
|
|
);
|
|
assert(
|
|
instructions.includes("Lock Claiming Protocol"),
|
|
"Instructions include header",
|
|
);
|
|
assert(
|
|
instructions.includes("Claim Types"),
|
|
"Instructions include claim types",
|
|
);
|
|
assert(
|
|
instructions.includes("Auto-Release Behavior"),
|
|
"Instructions include auto-release section",
|
|
);
|
|
assert(
|
|
instructions.includes("Conflict Resolution"),
|
|
"Instructions include conflict resolution",
|
|
);
|
|
assert(
|
|
instructions.includes("Best Practices"),
|
|
"Instructions include best practices",
|
|
);
|
|
assert(
|
|
instructions.includes("Releasing Claims"),
|
|
"Instructions include releasing claims",
|
|
);
|
|
console.log("✅ System prompt injection: instructions generated correctly");
|
|
|
|
// Test 2: Guidelines are generated
|
|
const guidelines = buildLockClaimingGuidelines();
|
|
assert(Array.isArray(guidelines), "Guidelines is an array");
|
|
assert(guidelines.length > 0, "Guidelines has entries");
|
|
assert(
|
|
guidelines.some((g: string) => g.includes("file_claiming_claim")),
|
|
"Guidelines mentions claim tool",
|
|
);
|
|
assert(
|
|
guidelines.some((g: string) => g.includes("file_claiming_release")),
|
|
"Guidelines mentions release tool",
|
|
);
|
|
assert(
|
|
guidelines.some((g: string) => g.includes("file_claiming_list")),
|
|
"Guidelines mentions list tool",
|
|
);
|
|
assert(
|
|
guidelines.some((g: string) => g.includes("file_claiming_check")),
|
|
"Guidelines mentions check tool",
|
|
);
|
|
console.log("✅ System prompt injection: guidelines generated correctly");
|
|
|
|
// Test 3: Tool snippets are generated
|
|
const snippets = buildLockClaimingToolSnippets();
|
|
assert(snippets.file_claiming_claim, "Snippet for claim tool");
|
|
assert(snippets.file_claiming_release, "Snippet for release tool");
|
|
assert(snippets.file_claiming_list, "Snippet for list tool");
|
|
assert(snippets.file_claiming_check, "Snippet for check tool");
|
|
console.log("✅ System prompt injection: tool snippets generated correctly");
|
|
|
|
// Test 4: Injection into prompt options
|
|
const options = injectLockClaimingIntoPrompt({ cwd: "." });
|
|
assert(options.promptGuidelines, "Injected options have guidelines");
|
|
assert(options.toolSnippets, "Injected options have tool snippets");
|
|
assert(
|
|
options.appendSystemPrompt,
|
|
"Injected options have appendSystemPrompt",
|
|
);
|
|
assert(
|
|
options.appendSystemPrompt!.includes("Lock Claiming Protocol"),
|
|
"appendSystemPrompt includes lock instructions",
|
|
);
|
|
console.log(
|
|
"✅ System prompt injection: injection into options works correctly",
|
|
);
|
|
|
|
// Test 5: Config values are substituted
|
|
const config = getConfig();
|
|
assert(
|
|
instructions.includes(String(config.autoReleaseTTL)),
|
|
"TTL value is in instructions",
|
|
);
|
|
console.log("✅ System prompt injection: config values substituted");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Diagnostic message tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testDiagnosticMessages() {
|
|
const {
|
|
claimToDiagnostic,
|
|
conflictToDiagnostic,
|
|
buildDiagnosticCollection,
|
|
formatDiagnostics,
|
|
getDiagnosticsWidgetContent,
|
|
formatRelativeTime,
|
|
hasActiveClaim,
|
|
getClaimsForPath,
|
|
getLockedFiles,
|
|
} = require("../src/diagnostics");
|
|
|
|
const registry = getClaimRegistry();
|
|
resetRegistry();
|
|
|
|
// Test 1: Claim to diagnostic
|
|
const testClaim: FileClaim = {
|
|
id: "test-1",
|
|
path: "/test/file.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
reason: "Editing file",
|
|
};
|
|
const diag = claimToDiagnostic(testClaim, registry);
|
|
assert(diag.uri === "/test/file.ts", "Diagnostic URI matches claim path");
|
|
assert(diag.severity === "warning", "Write lock has warning severity");
|
|
assert(diag.source === "file-claiming", "Diagnostic source is file-claiming");
|
|
assert(diag.code === "LOCK_WRITE", "Diagnostic code is correct");
|
|
assert(
|
|
diag.message.includes("test/file.ts"),
|
|
"Diagnostic message includes path",
|
|
);
|
|
assert(diag.tool === undefined, "Agent type has no tool field");
|
|
console.log("✅ Diagnostic messages: claim to diagnostic conversion works");
|
|
|
|
// Test 2: Conflict to diagnostic
|
|
const conflictDiag = conflictToDiagnostic("/test/file.ts", "read", [
|
|
{
|
|
path: "/test/file.ts",
|
|
lockType: "write",
|
|
claimId: "test-1",
|
|
owner: mockOwner("tool", "edit"),
|
|
acquiredAt: new Date().toISOString(),
|
|
},
|
|
]);
|
|
assert(conflictDiag.severity === "error", "Conflict has error severity");
|
|
assert(
|
|
conflictDiag.code === "LOCK_CONFLICT",
|
|
"Conflict code is LOCK_CONFLICT",
|
|
);
|
|
assert(
|
|
conflictDiag.message.includes("blocked"),
|
|
"Conflict message mentions blockers",
|
|
);
|
|
console.log(
|
|
"✅ Diagnostic messages: conflict to diagnostic conversion works",
|
|
);
|
|
|
|
// Test 3: Diagnostic collection
|
|
registry.acquire({
|
|
...testClaim,
|
|
id: "test-2",
|
|
path: "/test/file.ts",
|
|
lockType: "read",
|
|
});
|
|
registry.acquire({
|
|
...testClaim,
|
|
id: "test-3",
|
|
path: "/test/other.ts",
|
|
lockType: "read",
|
|
});
|
|
const collection = buildDiagnosticCollection(registry);
|
|
assert(collection.count > 0, "Collection has diagnostics");
|
|
assert(collection.diagnostics.size > 0, "Collection has entries");
|
|
assert(collection.bySeverity.info >= 0, "Info count is valid");
|
|
assert(collection.bySeverity.warning >= 0, "Warning count is valid");
|
|
assert(collection.bySeverity.error >= 0, "Error count is valid");
|
|
console.log("✅ Diagnostic messages: collection building works");
|
|
|
|
// Test 4: Formatting
|
|
const formatted = formatDiagnostics(collection);
|
|
assert(formatted.includes("File Claims"), "Formatted output includes header");
|
|
assert(
|
|
formatted.includes(collection.count.toString()),
|
|
"Formatted output includes count",
|
|
);
|
|
console.log("✅ Diagnostic messages: formatting works");
|
|
|
|
// Test 5: Widget content
|
|
const widgetContent = getDiagnosticsWidgetContent(registry);
|
|
assert(Array.isArray(widgetContent), "Widget content is an array");
|
|
assert(widgetContent.length > 0, "Widget content has entries");
|
|
assert(widgetContent[0].includes("Claims"), "Widget content mentions claims");
|
|
console.log("✅ Diagnostic messages: widget content generation works");
|
|
|
|
// Test 6: Relative time formatting
|
|
const now = new Date();
|
|
const future = new Date(now.getTime() + 60_000).toISOString();
|
|
assert(
|
|
formatRelativeTime(future).includes("1m"),
|
|
"Relative time shows minutes",
|
|
);
|
|
const nearFuture = new Date(now.getTime() + 30_000).toISOString();
|
|
assert(
|
|
formatRelativeTime(nearFuture).includes("30s"),
|
|
"Relative time shows seconds",
|
|
);
|
|
console.log("✅ Diagnostic messages: relative time formatting works");
|
|
|
|
// Test 7: Helper functions
|
|
assert(
|
|
hasActiveClaim(registry, "/test/file.ts"),
|
|
"hasActiveClaim returns true for claimed file",
|
|
);
|
|
assert(
|
|
!hasActiveClaim(registry, "/test/missing.ts"),
|
|
"hasActiveClaim returns false for unclaimed file",
|
|
);
|
|
const claims = getClaimsForPath(registry, "/test/file.ts");
|
|
assert(claims.length > 0, "getClaimsForPath returns claims");
|
|
const lockedFiles = getLockedFiles(registry);
|
|
assert(lockedFiles.length > 0, "getLockedFiles returns locked files");
|
|
console.log("✅ Diagnostic messages: helper functions work");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tool registration tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testToolRegistration() {
|
|
const {
|
|
registerLockTools,
|
|
fileClaimingClaimTool,
|
|
fileClaimingReleaseTool,
|
|
fileClaimingListTool,
|
|
fileClaimingCheckTool,
|
|
} = require("../src/tools");
|
|
|
|
// Test 1: Tools are defined
|
|
assert(
|
|
fileClaimingClaimTool.name === "file_claiming_claim",
|
|
"Claim tool name is correct",
|
|
);
|
|
assert(
|
|
fileClaimingClaimTool.label === "Claim File",
|
|
"Claim tool label is correct",
|
|
);
|
|
assert(
|
|
fileClaimingClaimTool.description.length > 0,
|
|
"Claim tool has description",
|
|
);
|
|
assert(fileClaimingClaimTool.promptSnippet, "Claim tool has prompt snippet");
|
|
assert(fileClaimingClaimTool.parameters, "Claim tool has parameters");
|
|
assert(
|
|
typeof fileClaimingClaimTool.execute === "function",
|
|
"Claim tool has execute function",
|
|
);
|
|
console.log("✅ Tool registration: claim tool is defined correctly");
|
|
|
|
assert(
|
|
fileClaimingReleaseTool.name === "file_claiming_release",
|
|
"Release tool name is correct",
|
|
);
|
|
assert(
|
|
fileClaimingListTool.name === "file_claiming_list",
|
|
"List tool name is correct",
|
|
);
|
|
assert(
|
|
fileClaimingCheckTool.name === "file_claiming_check",
|
|
"Check tool name is correct",
|
|
);
|
|
console.log("✅ Tool registration: all tool names are correct");
|
|
|
|
// Test 2: Tool descriptions are actionable
|
|
assert(
|
|
fileClaimingClaimTool.description.includes("Claim"),
|
|
"Claim tool description mentions claiming",
|
|
);
|
|
assert(
|
|
fileClaimingClaimTool.description.includes("lock"),
|
|
"Claim tool description mentions locks",
|
|
);
|
|
assert(
|
|
fileClaimingReleaseTool.description.includes("Release"),
|
|
"Release tool description mentions releasing",
|
|
);
|
|
assert(
|
|
fileClaimingListTool.description.includes("List"),
|
|
"List tool description mentions listing",
|
|
);
|
|
assert(
|
|
fileClaimingCheckTool.description.includes("Check"),
|
|
"Check tool description mentions checking",
|
|
);
|
|
console.log("✅ Tool registration: tool descriptions are actionable");
|
|
|
|
// Test 3: Prompt guidelines are clear
|
|
assert(
|
|
fileClaimingClaimTool.promptSnippet.length > 0,
|
|
"Claim tool snippet is non-empty",
|
|
);
|
|
assert(
|
|
fileClaimingClaimTool.promptSnippet.length < 80,
|
|
"Claim tool snippet is concise",
|
|
);
|
|
console.log("✅ Tool registration: prompt guidelines are clear");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Notification system tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testNotificationSystem() {
|
|
const {
|
|
claimEventToNotification,
|
|
formatNotification,
|
|
formatNotificationsSummary,
|
|
} = require("../src/notifications");
|
|
const { createDiagnosticEvent } = require("../src/diagnostics");
|
|
|
|
// Test 1: Claim acquired notification
|
|
const acquiredEvent = {
|
|
type: "claim:acquired",
|
|
claim: {
|
|
id: "test-1",
|
|
path: "/test/file.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
conflict: undefined,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
const acquiredNotif = claimEventToNotification(acquiredEvent as any);
|
|
assert(
|
|
acquiredNotif.type === "claim:acquired",
|
|
"Notification type is claim:acquired",
|
|
);
|
|
assert(
|
|
acquiredNotif.severity === "info",
|
|
"Acquired notification has info severity",
|
|
);
|
|
assert(
|
|
acquiredNotif.title.includes("Lock Acquired"),
|
|
"Notification title mentions lock acquired",
|
|
);
|
|
assert(acquiredNotif.claim, "Notification includes claim data");
|
|
console.log("✅ Notification system: claim acquired notification works");
|
|
|
|
// Test 2: Claim released notification
|
|
const releasedEvent = {
|
|
type: "claim:released",
|
|
claim: acquiredEvent.claim,
|
|
conflict: undefined,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
const releasedNotif = claimEventToNotification(releasedEvent as any);
|
|
assert(
|
|
releasedNotif.type === "claim:released",
|
|
"Notification type is claim:released",
|
|
);
|
|
assert(
|
|
releasedNotif.title === "Lock Released",
|
|
"Notification title is Lock Released",
|
|
);
|
|
console.log("✅ Notification system: claim released notification works");
|
|
|
|
// Test 3: Claim conflicted notification
|
|
const conflictedEvent = {
|
|
type: "claim:conflicted",
|
|
claim: acquiredEvent.claim,
|
|
conflict: {
|
|
path: "/test/file.ts",
|
|
severity: "warning",
|
|
blockedClaim: acquiredEvent.claim,
|
|
blockingClaims: [],
|
|
message: "Cannot acquire lock",
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
const conflictedNotif = claimEventToNotification(conflictedEvent as any);
|
|
assert(
|
|
conflictedNotif.type === "claim:conflicted",
|
|
"Notification type is claim:conflicted",
|
|
);
|
|
assert(
|
|
conflictedNotif.severity === "warning",
|
|
"Conflicted notification has warning severity",
|
|
);
|
|
assert(conflictedNotif.conflict, "Notification includes conflict data");
|
|
console.log("✅ Notification system: claim conflicted notification works");
|
|
|
|
// Test 4: Claim expired notification
|
|
const expiredEvent = {
|
|
type: "claim:expired",
|
|
claim: acquiredEvent.claim,
|
|
conflict: undefined,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
const expiredNotif = claimEventToNotification(expiredEvent as any);
|
|
assert(
|
|
expiredNotif.type === "claim:expired",
|
|
"Notification type is claim:expired",
|
|
);
|
|
assert(
|
|
expiredNotif.title === "Lock Expired",
|
|
"Notification title is Lock Expired",
|
|
);
|
|
console.log("✅ Notification system: claim expired notification works");
|
|
|
|
// Test 5: Notification formatting
|
|
const formatted = formatNotification(acquiredNotif);
|
|
assert(
|
|
formatted.includes(acquiredNotif.title),
|
|
"Formatted notification includes title",
|
|
);
|
|
assert(
|
|
formatted.includes(acquiredNotif.message),
|
|
"Formatted notification includes message",
|
|
);
|
|
console.log("✅ Notification system: notification formatting works");
|
|
|
|
// Test 6: Summary formatting
|
|
const notifications = [
|
|
acquiredNotif,
|
|
releasedNotif,
|
|
conflictedNotif,
|
|
expiredNotif,
|
|
];
|
|
const summary = formatNotificationsSummary(notifications);
|
|
assert(summary.includes("Lock Notifications"), "Summary includes header");
|
|
assert(summary.includes("4"), "Summary includes count");
|
|
console.log("✅ Notification system: summary formatting works");
|
|
|
|
// Test 7: Diagnostic events
|
|
const diagEvent = createDiagnosticEvent("diagnostic:added", "/test/file.ts", {
|
|
uri: "/test/file.ts",
|
|
severity: "info",
|
|
source: "file-claiming",
|
|
code: "LOCK_READ",
|
|
message: "Read lock on file",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
assert(
|
|
diagEvent.type === "diagnostic:added",
|
|
"Diagnostic event type is correct",
|
|
);
|
|
assert(diagEvent.uri === "/test/file.ts", "Diagnostic event URI is correct");
|
|
console.log("✅ Notification system: diagnostic events work");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// User interaction component tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testUserInteractionComponents() {
|
|
const {
|
|
createLockStatusWidget,
|
|
updateLockStatus,
|
|
persistLockState,
|
|
restoreLockState,
|
|
} = require("../src/user-interaction");
|
|
|
|
// Test 1: Lock status widget
|
|
const registry = getClaimRegistry();
|
|
resetRegistry();
|
|
|
|
registry.acquire({
|
|
id: "widget-test",
|
|
path: "/test/widget.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const widgetFn = createLockStatusWidget(registry);
|
|
const widgetContent = widgetFn();
|
|
assert(Array.isArray(widgetContent), "Widget returns array");
|
|
assert(widgetContent.length > 0, "Widget has content");
|
|
assert(
|
|
widgetContent.some((line: string) => line.includes("Claims")),
|
|
"Widget mentions claims",
|
|
);
|
|
console.log("✅ User interaction: lock status widget works");
|
|
|
|
// Test 2: Status bar update
|
|
const mockUI = {
|
|
setStatus: (key: string, text: string | undefined) => {},
|
|
};
|
|
updateLockStatus(mockUI as any, registry);
|
|
console.log("✅ User interaction: status bar update works");
|
|
|
|
// Test 3: State persistence
|
|
const mockPi = {
|
|
appendEntry: (type: string, data: unknown) => {},
|
|
getSessionName: () => "test-session",
|
|
};
|
|
persistLockState(mockPi as any);
|
|
console.log("✅ User interaction: state persistence works");
|
|
|
|
// Test 4: State restoration
|
|
const restored = restoreLockState(mockPi as any);
|
|
assert(typeof restored === "boolean", "Restore returns boolean");
|
|
console.log("✅ User interaction: state restoration works");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integration test: full flow
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testFullIntegration() {
|
|
const registry = getClaimRegistry();
|
|
resetRegistry();
|
|
resetConfig();
|
|
|
|
// Set up config
|
|
setConfig({ showDiagnostics: true, autoReleaseTTL: 5000 });
|
|
|
|
// Create a claim
|
|
const owner = mockOwner("agent", "main");
|
|
registry.acquire({
|
|
id: "integration-1",
|
|
path: "/test/integration.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
// Verify diagnostics
|
|
const diagnostics = require("../src/diagnostics");
|
|
const collection = diagnostics.buildDiagnosticCollection(registry);
|
|
assert(collection.count > 0, "Integration: diagnostics have entries");
|
|
|
|
// Verify notifications
|
|
const notifications = require("../src/notifications");
|
|
const notif = notifications.claimEventToNotification({
|
|
type: "claim:acquired",
|
|
claim: registry.claims["integration-1"],
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
assert(
|
|
notif.severity === "info",
|
|
"Integration: notification has correct severity",
|
|
);
|
|
|
|
// Verify system prompt injection
|
|
const systemPrompt = require("../src/system-prompt");
|
|
const options = systemPrompt.injectLockClaimingIntoPrompt({ cwd: "." });
|
|
assert(
|
|
options.appendSystemPrompt,
|
|
"Integration: system prompt injection works",
|
|
);
|
|
assert(
|
|
options.appendSystemPrompt!.includes("Lock Claiming Protocol"),
|
|
"Integration: lock instructions present",
|
|
);
|
|
|
|
// Verify tool registration
|
|
const tools = require("../src/tools");
|
|
assert(
|
|
tools.fileClaimingClaimTool.name === "file_claiming_claim",
|
|
"Integration: tools are defined",
|
|
);
|
|
|
|
// Clean up
|
|
registry.release("integration-1");
|
|
console.log("✅ Full integration test: complete flow works");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Lock acquisition tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testLockAcquisition() {
|
|
const {
|
|
acquireLock,
|
|
autoClaim,
|
|
isFileLocked,
|
|
getLockInfo,
|
|
} = require("../src/lock-acquisition");
|
|
const registry = getClaimRegistry();
|
|
const owner = mockOwner("agent", "main");
|
|
|
|
// Test 1: Lock acquisition succeeds for unclaimed files
|
|
resetRegistry();
|
|
const acqResult = acquireLock({
|
|
path: "/test/acquire.ts",
|
|
lockType: "write",
|
|
owner,
|
|
autoReleaseTTL: 5000,
|
|
});
|
|
assert(acqResult.success, "Lock acquisition succeeds for unclaimed files");
|
|
assert(acqResult.claim, "Lock acquisition returns a claim");
|
|
assert(acqResult.claim!.path === "/test/acquire.ts", "Claim path matches");
|
|
assert(acqResult.claim!.lockType === "write", "Claim lock type matches");
|
|
assert(acqResult.autoClaimed, "Auto-claimed flag is set");
|
|
assert(
|
|
acqResult.message.includes("Auto-claimed"),
|
|
"Message mentions auto-claim",
|
|
);
|
|
console.log("✅ Lock acquisition: unclaimed file acquisition works");
|
|
|
|
// Test 2: Auto-claim logic triggers correctly
|
|
resetRegistry();
|
|
const autoResult = autoClaim({
|
|
path: "/test/auto.ts",
|
|
lockType: "write",
|
|
owner,
|
|
autoReleaseTTL: 3000,
|
|
});
|
|
assert(autoResult.success, "Auto-claim succeeds");
|
|
assert(autoResult.autoClaimed, "Auto-claim sets autoClaimed flag");
|
|
assert(
|
|
autoResult.claim!.owner.type === "agent",
|
|
"Auto-claim uses correct owner",
|
|
);
|
|
assert(
|
|
autoResult.claim!.owner.id === "main",
|
|
"Auto-claim uses correct owner id",
|
|
);
|
|
console.log("✅ Lock acquisition: auto-claim logic triggers correctly");
|
|
|
|
// Test 3: Blocking mechanism prevents access to locked files
|
|
resetRegistry();
|
|
registry.acquire({
|
|
id: "block-test",
|
|
path: "/test/blocked.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const blocked = isFileLocked("/test/blocked.ts", "write");
|
|
assert(blocked, "isFileLocked returns true for write-locked file");
|
|
|
|
const unblocked = isFileLocked("/test/blocked.ts", "read");
|
|
// Read should be blocked when write lock exists
|
|
assert(unblocked, "isFileLocked returns true for read on write-locked file");
|
|
|
|
const free = isFileLocked("/test/fresh.ts", "write");
|
|
assert(!free, "isFileLocked returns false for unclaimed file");
|
|
console.log(
|
|
"✅ Lock acquisition: blocking mechanism prevents access to locked files",
|
|
);
|
|
|
|
// Test 4: Lock info contains detailed information
|
|
resetRegistry();
|
|
registry.acquire({
|
|
id: "info-test",
|
|
path: "/test/info.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const info = getLockInfo("/test/info.ts");
|
|
assert(info.locked, "Lock info shows locked");
|
|
assert(info.path === "/test/info.ts", "Lock info has correct path");
|
|
assert(info.claims.length > 0, "Lock info has claims");
|
|
assert(info.locks.length > 0, "Lock info has locks");
|
|
assert(info.primaryLock, "Lock info has primary lock");
|
|
assert(info.primaryClaim, "Lock info has primary claim");
|
|
assert(info.autoReleaseAt, "Lock info has auto-release time");
|
|
assert(info.autoReleaseIn, "Lock info has auto-release in");
|
|
console.log("✅ Lock acquisition: lock info contains detailed information");
|
|
|
|
// Test 5: Concurrent access is handled
|
|
resetRegistry();
|
|
const owner1 = mockOwner("agent", "main");
|
|
const owner2 = mockOwner("agent", "other");
|
|
|
|
registry.acquire({
|
|
id: "concurrent-1",
|
|
path: "/test/concurrent.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: owner1,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const acq2 = acquireLock({
|
|
path: "/test/concurrent.ts",
|
|
lockType: "write",
|
|
owner: owner2,
|
|
autoReleaseTTL: 5000,
|
|
});
|
|
|
|
// owner2 should get a conflict since owner1 has write lock
|
|
assert(!acq2.success || acq2.conflict, "Concurrent access returns conflict");
|
|
console.log("✅ Lock acquisition: concurrent access is handled");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Event handler tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testEventHandlers() {
|
|
const {
|
|
createToolCallHandler,
|
|
createTurnEndHandler,
|
|
createSessionShutdownHandler,
|
|
createBeforeAgentStartHandler,
|
|
createContextHandler,
|
|
createSessionStartHandler,
|
|
} = require("../src/event-handlers");
|
|
const registry = getClaimRegistry();
|
|
|
|
// Test 1: tool_call handler intercepts edit/write operations
|
|
resetRegistry();
|
|
const toolHandler = 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 = toolHandler(editEvent, mockCtx);
|
|
assert(
|
|
result !== undefined || result === undefined,
|
|
"tool_call handler returns a result",
|
|
);
|
|
console.log(
|
|
"✅ Event handlers: tool_call handler intercepts edit/write operations",
|
|
);
|
|
|
|
// Test 2: turn_end handler triggers automatic release
|
|
resetRegistry();
|
|
setConfig({ releaseOnTurnEnd: true });
|
|
|
|
registry.acquire({
|
|
id: "turn-test",
|
|
path: "/test/turn.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const turnHandler = createTurnEndHandler();
|
|
turnHandler(
|
|
{
|
|
type: "turn_end",
|
|
turnIndex: 1,
|
|
message: {} as any,
|
|
toolResults: [],
|
|
},
|
|
mockCtx,
|
|
);
|
|
|
|
const remaining = registry.getActiveClaims("/test/turn.ts");
|
|
assert(remaining.length === 0, "turn_end handler releases agent claims");
|
|
console.log("✅ Event handlers: turn_end handler triggers automatic release");
|
|
|
|
// Test 3: session_shutdown handler cleans up all claims
|
|
resetRegistry();
|
|
registry.acquire({
|
|
id: "shutdown-1",
|
|
path: "/test/shutdown.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const shutdownHandler = createSessionShutdownHandler();
|
|
shutdownHandler({ type: "session_shutdown", reason: "quit" });
|
|
|
|
const afterShutdown = Object.values(registry.claims).filter(
|
|
(c) => c.status === "active",
|
|
);
|
|
assert(
|
|
afterShutdown.length === 0,
|
|
"session_shutdown handler cleans up all claims",
|
|
);
|
|
console.log(
|
|
"✅ Event handlers: session_shutdown handler cleans up all claims",
|
|
);
|
|
|
|
// Test 4: before_agent_start handler injects correct system prompt
|
|
resetConfig();
|
|
setConfig({ showDiagnostics: true });
|
|
|
|
const agentStartHandler = createBeforeAgentStartHandler();
|
|
const agentStartResult = agentStartHandler(
|
|
{
|
|
type: "before_agent_start",
|
|
prompt: "Test",
|
|
systemPrompt: "Initial",
|
|
systemPromptOptions: { cwd: "." },
|
|
},
|
|
mockCtx,
|
|
);
|
|
|
|
assert(
|
|
agentStartResult !== undefined,
|
|
"before_agent_start handler returns a result",
|
|
);
|
|
console.log(
|
|
"✅ Event handlers: before_agent_start handler injects correct system prompt",
|
|
);
|
|
|
|
// Test 5: context handler injects diagnostic messages
|
|
resetRegistry();
|
|
setConfig({ showDiagnostics: true });
|
|
|
|
registry.acquire({
|
|
id: "ctx-test",
|
|
path: "/test/context.ts",
|
|
lockType: "write",
|
|
status: "active",
|
|
owner: mockOwner("agent", "main"),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
const contextHandler = createContextHandler();
|
|
const contextResult = contextHandler(
|
|
{
|
|
type: "context",
|
|
messages: [{ role: "user", content: [{ type: "text", text: "Test" }] }],
|
|
},
|
|
mockCtx,
|
|
);
|
|
|
|
assert(
|
|
contextResult !== undefined,
|
|
"context handler injects diagnostic messages",
|
|
);
|
|
console.log("✅ Event handlers: context handler injects diagnostic messages");
|
|
|
|
// Test 6: session_start handler performs initialization
|
|
resetRegistry();
|
|
setConfig({ showDiagnostics: true });
|
|
|
|
const mockPi = {
|
|
registerTool: () => {},
|
|
events: { emit: () => {}, on: () => () => {} },
|
|
};
|
|
const sessionStartHandler = createSessionStartHandler(mockPi as any);
|
|
sessionStartHandler({ type: "session_start", reason: "startup" }, mockCtx);
|
|
|
|
assert(true, "session_start handler performs initialization");
|
|
console.log(
|
|
"✅ Event handlers: session_start handler performs initialization",
|
|
);
|
|
|
|
// Test 7: Integration - event handler coordination across lifecycle
|
|
resetRegistry();
|
|
setConfig({
|
|
showDiagnostics: true,
|
|
releaseOnTurnEnd: true,
|
|
autoReleaseTTL: 300_000,
|
|
blockedTools: ["edit", "write"],
|
|
});
|
|
|
|
// Session start
|
|
sessionStartHandler({ type: "session_start", reason: "startup" }, mockCtx);
|
|
|
|
// Tool call: edit a file
|
|
const editEvent2 = {
|
|
type: "tool_call",
|
|
toolName: "edit",
|
|
toolCallId: "edit-2",
|
|
input: { path: "/test/integration.ts" },
|
|
};
|
|
toolHandler(editEvent2, mockCtx);
|
|
|
|
const claimsAfterEdit = registry.getActiveClaims("/test/integration.ts");
|
|
assert(claimsAfterEdit.length > 0, "Integration: edit tool claims the file");
|
|
|
|
// Turn end: release agent claims
|
|
turnHandler(
|
|
{
|
|
type: "turn_end",
|
|
turnIndex: 1,
|
|
message: {} as any,
|
|
toolResults: [],
|
|
},
|
|
mockCtx,
|
|
);
|
|
|
|
const claimsAfterTurn = registry.getActiveClaims("/test/integration.ts");
|
|
assert(
|
|
claimsAfterTurn.length === 0,
|
|
"Integration: turn end releases agent claims",
|
|
);
|
|
|
|
// Session shutdown: clean up
|
|
shutdownHandler({ type: "session_shutdown", reason: "quit" });
|
|
|
|
const finalClaims = Object.values(registry.claims).filter(
|
|
(c) => c.status === "active",
|
|
);
|
|
assert(finalClaims.length === 0, "Integration: shutdown releases all claims");
|
|
|
|
console.log("✅ Event handlers: integration test passes");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test runner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function assert(condition: boolean, message: string): void {
|
|
if (!condition) {
|
|
throw new Error(`Assertion failed: ${message}`);
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log("Running File Claiming Extension LLM Integration Tests\n");
|
|
|
|
try {
|
|
testSystemPromptInjection();
|
|
testDiagnosticMessages();
|
|
testToolRegistration();
|
|
testNotificationSystem();
|
|
testUserInteractionComponents();
|
|
testFullIntegration();
|
|
testLockAcquisition();
|
|
testEventHandlers();
|
|
console.log("\n✅ All tests passed!");
|
|
} catch (err) {
|
|
console.error(`\n❌ Test failed: ${err}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTests();
|