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 10 saveIds: string[];11 12 strip(path: string): Promise<void>;13};141516const PNG = Uint8Array.from(17 atob(18 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",19 ),20 (c) => c.charCodeAt(0),21);2223const README = `# Demo2425A small repo for **Kepr's** tests.2627See [the guide](docs/guide.md), [the site](https://example.com) and [a trap](javascript:alert(1)).28293031<script>alert("raw html")</script>32`;33343536373839export async function makeKohBucket(repo = "demo"): Promise<Fixture> {40 const root = await mkdtemp(join(tmpdir(), "kepr-fixture-"));41 const home = join(root, "home");42 const bucketDir = join(root, "bucket");43 const repoDir = join(root, "work", repo);44 await mkdir(home);45 await mkdir(join(repoDir, "src/lib/deep"), { recursive: true });46 await mkdir(join(repoDir, "docs"));47 await mkdir(join(repoDir, "vendor"));48 const koh = (...args: string[]) => run(repoDir, home, args);4950 koh("init");51 koh("remote", "folder", bucketDir);52 await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me.\n");53 await writeFile(join(repoDir, "docs/logo.png"), PNG);54 await writeFile(join(repoDir, "secret.txt"), "hunter2\n");55 await writeFile(56 join(repoDir, "src/lib/deep/util.ts"),57 "// The *one* helper. Deep down.\nexport const one = 1;\n",58 );59 await writeFile(join(repoDir, "vendor/big.c"), "int x;\n".repeat(100));60 koh("save", "add docs, a secret and a deep helper");61 const first = JSON.parse(koh("log", "--json")).head_id as string;62 koh("flag", first, "--reason", "holds a secret");63 64 Bun.sleepSync(1100);65 await writeFile(join(repoDir, "README.md"), README);66 await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hi');\n");67 koh("save", "add readme and entry point", "--agent", "test-agent", "--session", "fixture");68 69 await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me. It is short.\n");70 koh("checkpoint", "before the second change");71 await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hello');\n");72 koh("save", "say hello");73 koh("offer");7475 const log = JSON.parse(koh("log", "--json")) as { saves: { id: string }[] };76 return {77 bucketDir,78 repo,79 repoDir,80 saveIds: log.saves.map((save) => save.id),81 strip: (path) => strip(repoDir, home, path),82 };83}8485function run(cwd: string, home: string, args: string[]): string {86 const result = Bun.spawnSync(["koh3", ...args], { cwd, env: env(home) });87 if (result.exitCode !== 0) {88 throw new Error(89 `koh3 ${args.join(" ")} failed: ${result.stderr.toString()}${result.stdout.toString()}`,90 );91 }92 return result.stdout.toString();93}9495function env(home: string) {96 return { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" };97}9899100async function strip(cwd: string, home: string, path: string): Promise<void> {101 let output = "";102 const proc = Bun.spawn(["koh3", "strip", path], {103 cwd,104 env: env(home),105 terminal: { cols: 120, rows: 24, data: (_term, data) => (output += data.toString()) },106 });107 proc.terminal?.write(`y\n${path}\n`);108 const code = await proc.exited;109 proc.terminal?.close();110 if (code !== 0) throw new Error(`koh3 strip ${path} failed: ${output}`);111}