/** * test-utils.ts — Shared test utilities and mocks for Pi session simulation. * * Provides: * - Mock owners and claims for test scenarios * - Mock Pi session simulation * - Temporary directory creation for integration tests * - Assertion helpers * * @module file-claiming/test-utils */ import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import type { ClaimOwner, FileClaim } from "../src/lock-types"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Default test session ID. */ export const TEST_SESSION_ID = "test-session-001"; /** Default owner for test claims. */ export const TEST_OWNER: ClaimOwner = { type: "agent", id: "test-agent", sessionId: TEST_SESSION_ID, }; /** Alternative session IDs for multi-session tests. */ export const SESSION_A = "session-alpha"; export const SESSION_B = "session-beta"; export const SESSION_C = "session-gamma"; /** Common test file paths. */ export const TEST_FILE_A = "/tmp/test-claiming/file-a.ts"; export const TEST_FILE_B = "/tmp/test-claiming/file-b.ts"; export const TEST_FILE_C = "/tmp/test-claiming/file-c.ts"; // --------------------------------------------------------------------------- // Factory helpers // --------------------------------------------------------------------------- /** * Create a mock ClaimOwner for use in tests. */ export function mockOwner( type: ClaimOwner["type"] = "agent", id: string = "test-agent", sessionId: string = TEST_SESSION_ID, ): ClaimOwner { return { type, id, sessionId }; } /** * Create a FileClaim for use in tests. */ export function createTestClaim(overrides: Partial = {}): FileClaim { const now = new Date().toISOString(); return { id: randomUUID(), path: TEST_FILE_A, lockType: "write", status: "active", owner: TEST_OWNER, createdAt: now, updatedAt: now, expiresAt: new Date(Date.now() + 300_000).toISOString(), ...overrides, }; } // Cache for lazy-loaded modules let _resetRegistryCache: (() => void) | null = null; let _getActiveClaimsCache: (() => FileClaim[]) | null = null; /** * Reset the registry and config. * Uses dynamic import for ESM compatibility. */ export async function resetRegistry(): Promise { try { if (!_resetRegistryCache) { const mod = await import("../index"); const cfg = await import("../src/config"); const fn = () => { mod.resetRegistry(); cfg.resetConfig(); }; _resetRegistryCache = fn; fn(); } else { _resetRegistryCache(); } } catch { // If index.ts can't be loaded, just try config try { const cfg = await import("../src/config"); cfg.resetConfig(); } catch { // ignore } } } /** * Get the current active claims from the registry. */ export async function getActiveClaims(): Promise { try { if (!_getActiveClaimsCache) { const mod = await import("../index"); const fn = () => { const registry = mod.getClaimRegistry(); return Object.values(registry.claims).filter( (c: any) => c.status === "active", ); }; _getActiveClaimsCache = fn; return fn(); } return _getActiveClaimsCache(); } catch { return []; } } // --------------------------------------------------------------------------- // Mock Pi API // --------------------------------------------------------------------------- /** * Mock Pi extension API for testing event handlers and tool registration. */ export function createMockPi(): any { const handlers: Record any>> = {}; const entries: Array<{ type: string; data?: unknown }> = []; const registeredTools: string[] = []; return { events: { emit: (type: string, data?: unknown) => { const h = handlers[type] ?? []; for (const handler of h) handler(data); }, on: (type: string, handler: (...args: any[]) => any) => { (handlers[type] ??= []).push(handler); return () => { const idx = handlers[type]?.indexOf(handler) ?? -1; if (idx >= 0) handlers[type]!.splice(idx, 1); }; }, }, registerTool: (_tool: any) => { registeredTools.push(_tool.name ?? "unknown"); }, registerCommand: (_name: string, _def: any) => {}, appendEntry: (type: string, data?: unknown) => { entries.push({ type, data }); }, getSessionName: () => "test-session", on: (type: string, handler: (...args: any[]) => any) => { (handlers[type] ??= []).push(handler); return () => { const idx = handlers[type]?.indexOf(handler) ?? -1; if (idx >= 0) handlers[type]!.splice(idx, 1); }; }, _handlers: handlers, _entries: entries, _registeredTools: registeredTools, }; } /** * Create a mock ExtensionContext for testing event handlers. */ export function createMockContext(overrides: Record = {}): any { return { ui: { setWidget: () => {}, setStatus: () => {}, notify: () => {}, select: async () => "View all claims", input: async () => "/test/path.ts", confirm: async () => true, }, hasUI: true, cwd: "/tmp/test-claiming", sessionManager: { getSessionFile: () => TEST_SESSION_ID, }, modelRegistry: {}, model: undefined, isIdle: () => false, signal: undefined, abort: () => {}, hasPendingMessages: () => false, shutdown: () => {}, getContextUsage: () => undefined, compact: () => {}, getSystemPrompt: () => "", registerTool: () => {}, events: { emit: () => {}, on: () => () => {}, }, appendEntry: () => {}, ...overrides, }; } // --------------------------------------------------------------------------- // Temporary directory helpers // --------------------------------------------------------------------------- /** * Create a temporary directory for file-based integration tests. */ export function createTempDir(prefix: string = "file-claiming-test-"): string { return mkdtempSync(join(tmpdir(), prefix)); } /** * Remove a temporary directory and all its contents. */ export function cleanupTempDir(dir: string): void { rmSync(dir, { recursive: true, force: true }); } // --------------------------------------------------------------------------- // Assertion helpers // --------------------------------------------------------------------------- /** * Assert function for test validation. */ export function assert(condition: boolean, message: string): void { if (!condition) { throw new Error(`Assertion failed: ${message}`); } } /** * Measure the execution time of a synchronous function. */ export function measureTimeSync(fn: () => T): { result: T; elapsedMs: number; } { const start = performance.now(); const result = fn(); const elapsedMs = performance.now() - start; return { result, elapsedMs }; } /** * Measure the execution time of an async function. */ export async function measureTime( fn: () => Promise, ): Promise<{ result: T; elapsedMs: number }> { const start = performance.now(); const result = await fn(); const elapsedMs = performance.now() - start; return { result, elapsedMs }; }