271 lines
7.1 KiB
TypeScript
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 };
|
|
}
|