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

271 lines
7.1 KiB
TypeScript

/**
* 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> = {}): 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<void> {
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<FileClaim[]> {
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<string, Array<(...args: any[]) => 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<string, any> = {}): 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<T>(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<T>(
fn: () => Promise<T>,
): Promise<{ result: T; elapsedMs: number }> {
const start = performance.now();
const result = await fn();
const elapsedMs = performance.now() - start;
return { result, elapsedMs };
}