fixture.ts

4.1 KB · raw

1import { mkdir, mkdtemp, writeFile } from "node:fs/promises";2import { tmpdir } from "node:os";3import { join } from "node:path";45export type Fixture = {6	bucketDir: string;7	repo: string;8	repoDir: string;9	/** Every save, newest first, checkpoint included. */10	saveIds: string[];11	/** Strips a path from every save with koh3, so tests can sync before and after. */12	strip(path: string): Promise<void>;13};1415// The smallest valid PNG (1×1), so the fixture has a real binary file.16const PNG = Uint8Array.from(17	atob(18		"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",19	),20	(c) => c.charCodeAt(0),21);2223const README = `<!-- a leading comment and a centred title are not the intro -->2425<p align="center">DEMO</p>2627# Demo2829A small repo for **Kepr's** tests.3031See [the guide](docs/guide.md), [the site](https://example.com) and [a trap](javascript:alert(1)).3233![logo](docs/logo.png)3435<script>alert("raw html")</script>36`;3738/**39 * Builds a genuine Koh 3 folder remote with koh3 itself, under a throwaway HOME. The line is:40 * "add docs, a secret and a deep helper" (flagged) → a second later "add readme and entry point"41 * (agent and session) → a checkpoint holding an edit to the guide → "say hello" (head).42 */43export async function makeKohBucket(repo = "demo"): Promise<Fixture> {44	const root = await mkdtemp(join(tmpdir(), "kepr-fixture-"));45	const home = join(root, "home");46	const bucketDir = join(root, "bucket");47	const repoDir = join(root, "work", repo);48	await mkdir(home);49	await mkdir(join(repoDir, "src/lib/deep"), { recursive: true });50	await mkdir(join(repoDir, "docs"));51	await mkdir(join(repoDir, "vendor"));52	const koh = (...args: string[]) => run(repoDir, home, args);5354	koh("init");55	koh("remote", "folder", bucketDir);56	await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me.\n");57	await writeFile(join(repoDir, "docs/logo.png"), PNG);58	await writeFile(join(repoDir, "secret.txt"), "hunter2\n");59	await writeFile(60		join(repoDir, "src/lib/deep/util.ts"),61		"// The *one* helper. Deep down.\nexport const one = 1;\n",62	);63	await writeFile(join(repoDir, "vendor/big.c"), "int x;\n".repeat(100));64	koh("save", "add docs, a secret and a deep helper");65	const first = JSON.parse(koh("log", "--json")).head_id as string;66	koh("flag", first, "--reason", "holds a secret");67	// Saves a second apart, so the timeline has time to spread over.68	Bun.sleepSync(1100);69	await writeFile(join(repoDir, "README.md"), README);70	await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hi');\n");71	koh("save", "add readme and entry point", "--agent", "test-agent", "--session", "fixture");72	// The guide's edit is first recorded by the checkpoint; the Atlas must credit it to "say hello".73	await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me. It is short.\n");74	koh("checkpoint", "before the second change");75	await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hello');\n");76	koh("save", "say hello");77	koh("offer");7879	const log = JSON.parse(koh("log", "--json")) as { saves: { id: string }[] };80	return {81		bucketDir,82		repo,83		repoDir,84		saveIds: log.saves.map((save) => save.id),85		strip: (path) => strip(repoDir, home, path),86	};87}8889function run(cwd: string, home: string, args: string[]): string {90	const result = Bun.spawnSync(["koh3", ...args], { cwd, env: env(home) });91	if (result.exitCode !== 0) {92		throw new Error(93			`koh3 ${args.join(" ")} failed: ${result.stderr.toString()}${result.stdout.toString()}`,94		);95	}96	return result.stdout.toString();97}9899function env(home: string) {100	return { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" };101}102103// `koh strip` insists on a terminal and a typed confirmation, so it runs under a Bun PTY.104async function strip(cwd: string, home: string, path: string): Promise<void> {105	let output = "";106	const proc = Bun.spawn(["koh3", "strip", path], {107		cwd,108		env: env(home),109		terminal: { cols: 120, rows: 24, data: (_term, data) => (output += data.toString()) },110	});111	proc.terminal?.write(`y\n${path}\n`);112	const code = await proc.exited;113	proc.terminal?.close();114	if (code !== 0) throw new Error(`koh3 strip ${path} failed: ${output}`);115}