Repository Public
1/** Test helpers (not exported from the package index). */2import { execFileSync } from 'node:child_process';3import { createHash, randomBytes } from 'node:crypto';4import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';5import os from 'node:os';6import path from 'node:path';7import { cleanupRunTmp } from './sandbox.js';8import { applyFileStateDelta, type FileState, type Tool, type ToolContext, type ToolRunResult } from './types.js';910export interface Fixture {11 root: string; // temp root (real path)12 ws: string; // workspace13 dataDir: string;14 runId: string;15 ctx: ToolContext;16 artifacts: Map<string, Buffer>;17 /** Run a tool like the engine does: validate with zod, pass the folded file state, fold fileStateOut. */18 call(tool: Tool, input: unknown, over?: Partial<ToolContext>): Promise<ToolRunResult>;19 state(): FileState;20 cleanup(): Promise<void>;21}2223export function makeFixture(opts: { git?: boolean } = {}): Fixture {24 const root = mkdtempSync(path.join(realpathSync(os.tmpdir()), 'aos-tools-test-'));25 const ws = path.join(root, 'ws');26 const dataDir = path.join(root, 'data');27 mkdirSync(ws);28 mkdirSync(dataDir);29 const runId = `test-${randomBytes(6).toString('hex')}`;30 const artifacts = new Map<string, Buffer>();31 let state: FileState = {};32 let n = 0;33 const ctx: ToolContext = {34 runId,35 missionId: 'mission-0001',36 workspaceAbs: ws,37 network: 'none',38 dataDir,39 signal: new AbortController().signal,40 idemKey: 'idem-0',41 async putArtifact(name, data) {42 const buf = typeof data === 'string' ? Buffer.from(data) : data;43 const sha256 = createHash('sha256').update(buf).digest('hex');44 artifacts.set(name, buf);45 const dir = path.join(dataDir, 'artifacts', sha256.slice(0, 2));46 mkdirSync(dir, { recursive: true });47 writeFileSync(path.join(dir, sha256), buf);48 return { sha256, size: buf.length };49 },50 };51 if (opts.git) {52 git(ws, 'init', '-q', '-b', 'main', '.');53 git(ws, 'config', 'user.name', 'Test');54 git(ws, 'config', 'user.email', 'test@example.invalid');55 writeFileSync(path.join(ws, 'README.md'), '# test\n');56 git(ws, 'add', '-A');57 git(ws, 'commit', '-q', '-m', 'init');58 }59 return {60 root, ws, dataDir, runId, ctx, artifacts,61 async call(tool, input, over = {}) {62 const parsed = tool.input.parse(input);63 const res = await tool.run(parsed, { ...ctx, idemKey: `idem-${++n}`, fileState: state, ...over });64 state = applyFileStateDelta(state, res.fileStateOut);65 return res;66 },67 state: () => state,68 async cleanup() {69 await cleanupRunTmp(runId);70 rmSync(root, { recursive: true, force: true });71 },72 };73}7475export function git(cwd: string, ...args: string[]): string {76 return execFileSync('git', args, { cwd, encoding: 'utf8', env: { PATH: process.env.PATH ?? '', HOME: os.homedir(), GIT_TERMINAL_PROMPT: '0' } });77}78