SPB Git

Repository Public

testutil.ts 2.9 KB 78 lines typescript

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
Files main
Menu

Theme