test/cdp/target-store-test.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
/**
* Unit tests for the persisted CDP target — no browser required.
*
* The store is what makes /browser-target outlive the pi session that ran it,
* because the browser daemon is machine-wide. Its central obligation is that a
* damaged file degrades to "nothing stored" — falling back to the built-in
* default — rather than throwing and taking browser control down with it.
*
* Run: npx tsx test/cdp/target-store-test.ts
*/
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
let passed = 0;
let failed = 0;
const check = (cond: boolean, label: string): void => {
if (cond) {
passed++;
console.log(` ✓ ${label}`);
} else {
failed++;
console.error(` ✗ ${label}`);
}
};
async function main(): Promise<void> {
// The store resolves its path from pi's agent dir, which paths.ts derives
// from the environment — so point that at a scratch dir before importing.
const root = mkdtempSync(join(tmpdir(), "pi-target-"));
process.env["PI_CODING_AGENT_DIR"] = root;
const { readTarget, writeTarget, clearTarget } = await import("../../src/cdp/target-store");
const { targetFilePath } = await import("../../src/profile/paths");
const file = targetFilePath();
try {
// T1: nothing stored yet.
check((await readTarget()) === null, "T1: a missing file reads as nothing stored");
// T2: round-trip.
{
const w = await writeTarget("local");
check(w.success, "T2: writing a target succeeds");
check((await readTarget()) === "local", "T2: it reads back verbatim");
}
// T3: overwrite, not append — a switch must fully replace the last choice.
{
await writeTarget("10.4.5.6:9299");
check((await readTarget()) === "10.4.5.6:9299", "T3: a second write replaces the first");
}
// T4: clearing restores "nothing stored", which resolution reads as the
// built-in default rather than as local.
{
const c = await clearTarget();
check(c.success, "T4: clearing succeeds");
check((await readTarget()) === null, "T4: a cleared target reads as nothing stored");
}
// T5: whitespace is normalised on read, so a hand-edited file with a
// trailing newline inside the JSON string still resolves.
{
writeFileSync(file, JSON.stringify({ version: 1, target: " local ", savedAt: "" }));
check((await readTarget()) === "local", "T5: surrounding whitespace is trimmed");
}
// T6: damage must degrade to null, never throw. Each of these is a real
// failure mode: truncated write, hand-edit, or a file from a future version.
{
const damaged: ReadonlyArray<readonly [string, string]> = [
["not json at all", "T6: unparseable JSON"],
['{"version":1,"target":', "T6: truncated JSON"],
['{"version":2,"target":"local"}', "T6: a newer file version"],
['{"version":1,"target":42}', "T6: a non-string target"],
['{"version":1,"target":null}', "T6: an explicitly null target"],
['{"version":1}', "T6: a missing target field"],
["[]", "T6: a JSON array instead of an object"],
['{"version":1,"target":""}', "T6: an empty target string"],
];
for (const [contents, label] of damaged) {
writeFileSync(file, contents);
let threw = false;
let value: string | null = "unset";
try {
value = await readTarget();
} catch {
threw = true;
}
check(!threw && value === null, `${label} degrades to nothing stored`);
}
}
// T7: recovery — a damaged file must not poison later writes.
{
writeFileSync(file, "garbage");
const w = await writeTarget("local");
check(w.success, "T7: writing over a damaged file succeeds");
check((await readTarget()) === "local", "T7: the store recovers");
}
} finally {
rmSync(root, { recursive: true, force: true });
}
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
void main();
|