Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* clear, tail (with corruption defense), count, + the concurrency guarantee
* (parallel appends don't interleave or corrupt — T68).
*/
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, statSync, chmodSync } from "node:fs";
Expand Down
1 change: 1 addition & 0 deletions tests/batch.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mapWithConcurrency } from "../lib/batch.ts";
Expand Down
1 change: 1 addition & 0 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, utimesSync, writeFileSync, existsSync } from "node:fs";
Expand Down
1 change: 1 addition & 0 deletions tests/capability.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import type { Api, Model } from "@earendil-works/pi-ai";
Expand Down
1 change: 1 addition & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
Expand Down
1 change: 1 addition & 0 deletions tests/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* (sorted by `(provider, id)` so the registry's iteration order doesn't
* matter).
*/
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import type { Api, Model } from "@earendil-works/pi-ai";
Expand Down
50 changes: 45 additions & 5 deletions tests/delegate.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { isInsideRealPiDir, REAL_PI_DIR } from "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { Api, Model } from "@earendil-works/pi-ai";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
import {
callVisionModel,
delegateToVisionModel,
Expand Down Expand Up @@ -620,8 +622,8 @@ test("delegateToVisionModel: abort → code 'aborted', 0 calls, no fallback", as
});
// ── v0.5.0 (SPEC-5) tests: audit log + local-only mode ───────────────────

import { readFileSync, existsSync, mkdirSync } from "node:fs";
import { countAuditLog, tailAuditLog, clearAuditLog } from "../lib/audit.ts";
import { readFileSync, existsSync } from "node:fs";
import { countAuditLog, clearAuditLog } from "../lib/audit.ts";

/** Helper: a temp dir + an image file + a configured ctx + an audit log path. */
function setupAuditTest() {
Expand Down Expand Up @@ -881,3 +883,41 @@ test("T47 regression: single-image success with audit on → v0.4.0 behavior pre
});



// ── Guard: the agent-dir redirect from tests/setup.ts must stay in place ───
// Tests that leave auditLogPath unset resolve to getAgentDir(). Without the
// redirect, every one of them appends a line to the developer's real
// ~/.pi/agent/vision-audit.log — silently mixing fixture entries (fake
// providers, images that never existed) into the log that answers "where did
// my image bytes actually go?".
test("agent dir is redirected away from the real ~/.pi/agent", () => {
const agentDir = getAgentDir();
assert.ok(
process.env.PI_CODING_AGENT_DIR,
`agent dir must follow the redirect — is ./setup.ts still the first import?`,
);
assert.ok(
!isInsideRealPiDir(agentDir),
`agent dir must not be inside ${REAL_PI_DIR}, got ${agentDir}`,
);
});

// The redirect only holds for a file that installs it, and `tsx --test <file>`
// is the normal single-file loop — so a new test file that forgets the import
// silently writes to the real log. Cheaper to catch here than in the log.
test("every test file installs the agent-dir redirect first", () => {
const testsDir = dirname(fileURLToPath(import.meta.url));
const files = readdirSync(testsDir).filter((f) => f.endsWith(".test.ts"));
assert.ok(files.length > 1, "expected to find the test files");
for (const file of files) {
const source = readFileSync(join(testsDir, file), "utf8");
const firstImport = source
.split("\n")
.find((line) => /^import[\s{]/.test(line));
assert.match(
firstImport ?? "",
/^import (?:.+ from )?"\.\/setup\.ts";$/,
`${file} must import "./setup.ts" before anything else — otherwise it writes to the real ~/.pi/agent`,
);
}
});
1 change: 1 addition & 0 deletions tests/image.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
Expand Down
17 changes: 6 additions & 11 deletions tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* reason about the attached image / choose to call describe_image) still
* needs a manual fresh-session check — see the session handoff.
*/
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
Expand All @@ -25,18 +26,17 @@ import type {
ExtensionContext,
ToolDefinition,
} from "@earendil-works/pi-coding-agent";

// Redirect getAgentDir() to a temp dir so /vision commands never touch the
// real ~/.pi/agent/vision.json during this test run.
const TMP_AGENT = mkdtempSync(join(tmpdir(), "vision-eval-agent-"));
process.env.PI_CODING_AGENT_DIR = TMP_AGENT;

import visionFactory from "../extensions/vision.ts";
import pasteFactory from "../extensions/paste.ts";
import { loadConfig, configFilePath } from "../lib/config.ts";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import { countAuditLog, tailAuditLog } from "../lib/audit.ts";

// The redirect away from the real ~/.pi/agent is installed by ./setup.ts, which
// also owns the temp dir's lifetime; this is just the dir the /vision commands
// under test read and write.
const TMP_AGENT = getAgentDir();

const PNG_1x1_B64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M8AAAMBEg1+mP0AAAAASUVORK5CYII=";
const PNG_BYTES = Buffer.from(PNG_1x1_B64, "base64");
Expand Down Expand Up @@ -1982,8 +1982,3 @@ test("T70: v0.5.0 regression gate — config fields + subcommands + auto-detect
// Full suite green = T70 passed (if this test runs, the suite compiled + loaded).
assert.ok(true, "v0.5.0 surface wired + regression gate passed");
});

// Cleanup the temp agent dir after all tests.
test("cleanup", () => {
rmSync(TMP_AGENT, { recursive: true, force: true });
});
1 change: 1 addition & 0 deletions tests/marker.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import {
Expand Down
1 change: 1 addition & 0 deletions tests/normalize.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { normalizeImagePaths } from "../extensions/vision.ts";
Expand Down
1 change: 1 addition & 0 deletions tests/paste.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { findImagePathTokens } from "../extensions/paste.ts";
Expand Down
1 change: 1 addition & 0 deletions tests/preview.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import {
Expand Down
1 change: 1 addition & 0 deletions tests/resilience.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./setup.ts";
import { test } from "node:test";
import assert from "node:assert/strict";
import { AbortError, classifyError, sleep, withRetry, type ErrorClass } from "../lib/resilience.ts";
Expand Down
71 changes: 71 additions & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Agent-dir redirect for the test suite — imported as the first statement of
// every tests/*.test.ts file, so it takes effect however the suite is started:
// `pnpm test`, a bare `tsx --test`, or a single file. (A meta-test in
// delegate.test.ts enforces that every test file still does so.)
//
// Why: config, cache and the audit log all resolve through getAgentDir(), which
// reads PI_CODING_AGENT_DIR at call time and otherwise falls back to the real
// ~/.pi/agent. Any test that delegates with the default config (auditLog is on)
// would append fixture rows — fake providers, images that never existed — to the
// developer's production vision-audit.log.
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join, resolve, sep } from "node:path";

/** The real pi config root — nothing the suite writes may land inside it. */
export const REAL_PI_DIR = join(homedir(), ".pi");

/** Mirror of the tilde handling getAgentDir() applies to the env var, so the
* value we hand on is the one getAgentDir() will return. */
function expand(dir: string): string {
if (dir === "~") return homedir();
if (dir.startsWith("~/")) return join(homedir(), dir.slice(2));
return resolve(dir);
}

/** True if `dir` is ~/.pi itself or anything below it. Separator-aware: a
* sibling such as ~/.pi-sandbox is a legitimate test dir, not the real one. */
export function isInsideRealPiDir(dir: string): boolean {
const abs = expand(dir);
return abs === REAL_PI_DIR || abs.startsWith(REAL_PI_DIR + sep);
}

// Respect a dir supplied from outside (CI, a wrapper script) — but never one
// that points back into ~/.pi, which is exactly the pollution this file exists
// to prevent. Only the dir we created ourselves is ours to delete.
let ownDir: string | undefined;
const inherited = process.env.PI_CODING_AGENT_DIR;

if (inherited && !isInsideRealPiDir(inherited)) {
process.env.PI_CODING_AGENT_DIR = expand(inherited);
mkdirSync(process.env.PI_CODING_AGENT_DIR, { recursive: true });
} else {
if (inherited) {
console.warn(
`[tests/setup] ignoring PI_CODING_AGENT_DIR=${inherited} — it resolves inside ${REAL_PI_DIR}; using a temp dir instead`,
);
}
ownDir = mkdtempSync(join(tmpdir(), "vision-test-agent-"));
process.env.PI_CODING_AGENT_DIR = ownDir;
}

function cleanup(): void {
if (ownDir) rmSync(ownDir, { recursive: true, force: true });
}

process.on("exit", cleanup);

// `exit` never fires when we are terminated by a signal — a Ctrl-C on the suite
// reaches every per-file child process and would otherwise leak one temp dir
// each. rmSync({ force: true }) is idempotent, so the extra `exit` pass that
// process.exit() triggers is harmless.
for (const [signal, code] of [
["SIGINT", 130],
["SIGTERM", 143],
["SIGHUP", 129],
] as const) {
process.on(signal, () => {
cleanup();
process.exit(code);
});
}