Initial commit
This commit is contained in:
999
tests/index.test.ts
Normal file
999
tests/index.test.ts
Normal file
@@ -0,0 +1,999 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user