aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-27 00:27:08 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit15a537a99efba614e6e108722462bcb78ec6c3c4 (patch)
tree2ff20907fee8ace7a071470d491fe7808dce6a63
parentce5d808cb4cc3581d85b829656d1d478dc43e3a6 (diff)
test: port remaining suites to node:test
Mechanical port of the hand-rolled check() harness. Import-after-env ordering preserved so suites still read their sandbox dirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--test/deep-research/agent-config-test.ts72
-rw-r--r--test/deep-research/readability-test.ts91
-rw-r--r--test/deep-research/serp-parser-test.ts97
-rw-r--r--test/profile/browser-process-test.ts232
-rw-r--r--test/profile/launch-test.ts167
-rw-r--r--test/profile/list-test.ts158
-rw-r--r--test/profile/paths-test.ts202
7 files changed, 600 insertions, 419 deletions
diff --git a/test/deep-research/agent-config-test.ts b/test/deep-research/agent-config-test.ts
index 0e9dfe4..5859060 100644
--- a/test/deep-research/agent-config-test.ts
+++ b/test/deep-research/agent-config-test.ts
@@ -5,38 +5,48 @@
* non-existent web_search / web_fetch tools. Pure string checks over the
* markdown file — no browser, no subagent runtime.
*
- * Run: npx tsx test/deep-research/agent-config-test.ts
+ * Run: npm test
*/
+import { test, before, describe } from "node:test";
+import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
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}`);
- }
-};
-
-const agentPath = join(import.meta.dirname, "..", "..", ".pi", "agents", "web-search-researcher.md");
-const raw = readFileSync(agentPath, "utf8");
-
-const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
-const frontmatter = fmMatch ? (fmMatch[1] ?? "") : "";
-const body = fmMatch ? raw.slice(fmMatch[0].length) : raw;
-const toolsLine = frontmatter.split("\n").find((l) => l.startsWith("tools:")) ?? "";
-
-// S20
-check(toolsLine.includes("browser_web_search"), "S20: tools includes browser_web_search");
-check(toolsLine.includes("browser_read_page"), "S20: tools includes browser_read_page");
-check(!/\bweb_search\b/.test(toolsLine), "S20: tools does NOT include web_search");
-check(!/\bweb_fetch\b/.test(toolsLine), "S20: tools does NOT include web_fetch");
-check(/^\s*isolated:\s*true\s*$/m.test(frontmatter), "S20: isolated: true is present");
-check(!/WebSearch|WebFetch/.test(body), "S20: body has no WebSearch/WebFetch references");
-
-console.log(`\n${passed} passed, ${failed} failed`);
-if (failed > 0) process.exit(1);
+describe("web-search-researcher subagent config references only real tools", () => {
+ let frontmatter: string;
+ let body: string;
+ let toolsLine: string;
+
+ before(() => {
+ const agentPath = join(import.meta.dirname, "..", "..", ".pi", "agents", "web-search-researcher.md");
+ const raw = readFileSync(agentPath, "utf8");
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
+ frontmatter = fmMatch ? (fmMatch[1] ?? "") : "";
+ body = fmMatch ? raw.slice(fmMatch[0].length) : raw;
+ toolsLine = frontmatter.split("\n").find((l) => l.startsWith("tools:")) ?? "";
+ });
+
+ test("S20: tools includes browser_web_search", () => {
+ assert.ok(toolsLine.includes("browser_web_search"));
+ });
+
+ test("S20: tools includes browser_read_page", () => {
+ assert.ok(toolsLine.includes("browser_read_page"));
+ });
+
+ test("S20: tools does NOT include web_search", () => {
+ assert.ok(!/\bweb_search\b/.test(toolsLine));
+ });
+
+ test("S20: tools does NOT include web_fetch", () => {
+ assert.ok(!/\bweb_fetch\b/.test(toolsLine));
+ });
+
+ test("S20: isolated: true is present", () => {
+ assert.ok(/^\s*isolated:\s*true\s*$/m.test(frontmatter));
+ });
+
+ test("S20: body has no WebSearch/WebFetch references", () => {
+ assert.ok(!/WebSearch|WebFetch/.test(body));
+ });
+});
diff --git a/test/deep-research/readability-test.ts b/test/deep-research/readability-test.ts
index 10f9a8b..ba0e0e0 100644
--- a/test/deep-research/readability-test.ts
+++ b/test/deep-research/readability-test.ts
@@ -1,46 +1,67 @@
/**
* Unit tests for the pure readability extractor — no browser required.
*
- * Run: npx tsx test/deep-research/readability-test.ts
+ * Run: npm test
*/
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { extractReadable, type PageCapture } from "../../src/domains/readpage/readability";
-let passed = 0;
-let failed = 0;
-const check = (cond: boolean, label: string): void => {
- if (cond) {
- passed++;
- console.log(` ✓ ${label}`);
- } else {
- failed++;
- console.error(` ✗ ${label}`);
- }
-};
-
const fixture = (name: string): PageCapture =>
JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", name), "utf8")) as PageCapture;
-// S10: boilerplate stripped, article content preserved
-{
- const page = extractReadable(fixture("article-capture.json"));
- check(page.text.includes("Chrome DevTools Protocol lets tools instrument"), "S10: article body text is present");
- check(page.text.includes("Sessions and Targets"), "S10: article headings are preserved");
- check(!page.text.includes("Home Products Blog"), "S10: nav boilerplate is dropped");
- check(!page.text.includes("Copyright 2026"), "S10: footer boilerplate is dropped");
- check(!page.text.includes("Subscribe here for our newsletter"), "S10: high-link-density promo block is dropped");
- check(page.text.includes("## Sessions and Targets"), "S10: paragraph/heading structure preserved as markdown");
- check(page.title === "How the Chrome DevTools Protocol Works" && page.url.startsWith("https://"), "S10: title and url passed through");
- check(page.wordCount > 40, "S10: word count reflects the article body");
-}
-
-// S11: no clear article node falls back to bounded body text
-{
- const page = extractReadable(fixture("no-article-capture.json"));
- check(page.text.length > 0, "S11: structure-less page falls back to non-empty body text");
- check(page.text.includes("Your account is active"), "S11: fallback body text carries the real content");
-}
-
-console.log(`\n${passed} passed, ${failed} failed`);
-if (failed > 0) process.exit(1);
+describe("pure readability extractor", () => {
+ // S10: boilerplate stripped, article content preserved
+ test("S10: article body text is present", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(page.text.includes("Chrome DevTools Protocol lets tools instrument"));
+ });
+
+ test("S10: article headings are preserved", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(page.text.includes("Sessions and Targets"));
+ });
+
+ test("S10: nav boilerplate is dropped", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(!page.text.includes("Home Products Blog"));
+ });
+
+ test("S10: footer boilerplate is dropped", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(!page.text.includes("Copyright 2026"));
+ });
+
+ test("S10: high-link-density promo block is dropped", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(!page.text.includes("Subscribe here for our newsletter"));
+ });
+
+ test("S10: paragraph/heading structure preserved as markdown", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(page.text.includes("## Sessions and Targets"));
+ });
+
+ test("S10: title and url passed through", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(page.title === "How the Chrome DevTools Protocol Works" && page.url.startsWith("https://"));
+ });
+
+ test("S10: word count reflects the article body", () => {
+ const page = extractReadable(fixture("article-capture.json"));
+ assert.ok(page.wordCount > 40);
+ });
+
+ // S11: no clear article node falls back to bounded body text
+ test("S11: structure-less page falls back to non-empty body text", () => {
+ const page = extractReadable(fixture("no-article-capture.json"));
+ assert.ok(page.text.length > 0);
+ });
+
+ test("S11: fallback body text carries the real content", () => {
+ const page = extractReadable(fixture("no-article-capture.json"));
+ assert.ok(page.text.includes("Your account is active"));
+ });
+});
diff --git a/test/deep-research/serp-parser-test.ts b/test/deep-research/serp-parser-test.ts
index dabd72b..af5ac54 100644
--- a/test/deep-research/serp-parser-test.ts
+++ b/test/deep-research/serp-parser-test.ts
@@ -1,24 +1,14 @@
/**
* Unit tests for the pure Google SERP parser — no browser required.
*
- * Run: npx tsx test/deep-research/serp-parser-test.ts
+ * Run: npm test
*/
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { classifySerp, parseGoogleSerp, type SerpExtraction } from "../../src/domains/search/google-serp";
-let passed = 0;
-let failed = 0;
-const check = (cond: boolean, label: string): void => {
- if (cond) {
- passed++;
- console.log(` ✓ ${label}`);
- } else {
- failed++;
- console.error(` ✗ ${label}`);
- }
-};
-
const fixture = (name: string): SerpExtraction =>
JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", name), "utf8")) as SerpExtraction;
@@ -26,34 +16,53 @@ const ok = fixture("google-serp-ok.json");
const captcha = fixture("google-serp-captcha.json");
const empty = fixture("google-serp-empty.json");
-// S1: redirect unwrapping + internal/javascript anchors dropped
-{
- const results = parseGoogleSerp(ok.anchors, 10);
- const urls = results.map((r) => r.url);
- check(urls.includes("https://real.example/x"), "S1: /url?q= redirect is unwrapped to the real target");
- check(!urls.some((u) => u.includes("google.com")), "S1: google-internal anchors are dropped");
- check(!urls.some((u) => u.startsWith("javascript:")), "S1: javascript: anchors are dropped");
-}
-
-// S2: dedupe + rank + limit
-{
- const results = parseGoogleSerp(ok.anchors, 10);
- const realExampleCount = results.filter((r) => r.url.startsWith("https://real.example/x")).length;
- check(realExampleCount === 1, "S2: trailing-slash duplicate collapses to one result");
- check(
- results.every((r, i) => r.rank === i + 1),
- "S2: rank runs 1..n in input order",
- );
- const capped = parseGoogleSerp(ok.anchors, 2);
- check(capped.length === 2 && capped[capped.length - 1]?.rank === 2, "S2: limit truncates and ranks stay 1..limit");
-}
-
-// S3: CAPTCHA / no-results classification
-{
- check(classifySerp(ok.pageText, parseGoogleSerp(ok.anchors, 10).length) === "ok", "S3: populated SERP classifies as ok");
- check(classifySerp(captcha.pageText, 0) === "captcha", "S3: captcha page classifies as captcha");
- check(classifySerp(empty.pageText, 0) === "no_results", "S3: empty results classify as no_results");
-}
-
-console.log(`\n${passed} passed, ${failed} failed`);
-if (failed > 0) process.exit(1);
+describe("pure Google SERP parser", () => {
+ // S1: redirect unwrapping + internal/javascript anchors dropped
+ test("S1: /url?q= redirect is unwrapped to the real target", () => {
+ const results = parseGoogleSerp(ok.anchors, 10);
+ const urls = results.map((r) => r.url);
+ assert.ok(urls.includes("https://real.example/x"));
+ });
+
+ test("S1: google-internal anchors are dropped", () => {
+ const results = parseGoogleSerp(ok.anchors, 10);
+ const urls = results.map((r) => r.url);
+ assert.ok(!urls.some((u) => u.includes("google.com")));
+ });
+
+ test("S1: javascript: anchors are dropped", () => {
+ const results = parseGoogleSerp(ok.anchors, 10);
+ const urls = results.map((r) => r.url);
+ assert.ok(!urls.some((u) => u.startsWith("javascript:")));
+ });
+
+ // S2: dedupe + rank + limit
+ test("S2: trailing-slash duplicate collapses to one result", () => {
+ const results = parseGoogleSerp(ok.anchors, 10);
+ const realExampleCount = results.filter((r) => r.url.startsWith("https://real.example/x")).length;
+ assert.equal(realExampleCount, 1);
+ });
+
+ test("S2: rank runs 1..n in input order", () => {
+ const results = parseGoogleSerp(ok.anchors, 10);
+ assert.ok(results.every((r, i) => r.rank === i + 1));
+ });
+
+ test("S2: limit truncates and ranks stay 1..limit", () => {
+ const capped = parseGoogleSerp(ok.anchors, 2);
+ assert.ok(capped.length === 2 && capped[capped.length - 1]?.rank === 2);
+ });
+
+ // S3: CAPTCHA / no-results classification
+ test("S3: populated SERP classifies as ok", () => {
+ assert.equal(classifySerp(ok.pageText, parseGoogleSerp(ok.anchors, 10).length), "ok");
+ });
+
+ test("S3: captcha page classifies as captcha", () => {
+ assert.equal(classifySerp(captcha.pageText, 0), "captcha");
+ });
+
+ test("S3: empty results classify as no_results", () => {
+ assert.equal(classifySerp(empty.pageText, 0), "no_results");
+ });
+});
diff --git a/test/profile/browser-process-test.ts b/test/profile/browser-process-test.ts
index 29b3ddf..7a86c74 100644
--- a/test/profile/browser-process-test.ts
+++ b/test/profile/browser-process-test.ts
@@ -7,8 +7,10 @@
* never pointed us at gets a new window. (That is exactly what happened during
* development before ranking existed.)
*
- * Run: npx tsx test/profile/browser-process-test.ts
+ * Run: npm test
*/
+import { test, before, after, describe } from "node:test";
+import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
@@ -20,152 +22,174 @@ import {
stripDeletedSuffix,
} from "../../src/profile/browser-process";
-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> {
+describe("browser-process parsing and candidate ranking", () => {
// B1: --user-data-dir parsing
- {
- check(
- parseUserDataDirFlag("/opt/google/chrome/chrome --user-data-dir=/tmp/x --foo") === "/tmp/x",
- "B1: bare value parsed",
- );
- check(
- parseUserDataDirFlag('chrome.exe --user-data-dir="C:\\Users\\Some Name\\Data" --bar') ===
- "C:\\Users\\Some Name\\Data",
- "B1: double-quoted value with spaces parsed",
- );
- check(
- parseUserDataDirFlag("chrome --user-data-dir='/home/u/my dir'") === "/home/u/my dir",
- "B1: single-quoted value parsed",
+ test("B1: bare value parsed", () => {
+ assert.equal(parseUserDataDirFlag("/opt/google/chrome/chrome --user-data-dir=/tmp/x --foo"), "/tmp/x");
+ });
+
+ test("B1: double-quoted value with spaces parsed", () => {
+ assert.equal(
+ parseUserDataDirFlag('chrome.exe --user-data-dir="C:\\Users\\Some Name\\Data" --bar'),
+ "C:\\Users\\Some Name\\Data",
);
- check(parseUserDataDirFlag("/opt/google/chrome/chrome") === undefined, "B1: absent flag → undefined");
- check(parseUserDataDirFlag("chrome --user-data-dir=") === undefined, "B1: empty value → undefined");
- }
+ });
+
+ test("B1: single-quoted value parsed", () => {
+ assert.equal(parseUserDataDirFlag("chrome --user-data-dir='/home/u/my dir'"), "/home/u/my dir");
+ });
+
+ test("B1: absent flag → undefined", () => {
+ assert.equal(parseUserDataDirFlag("/opt/google/chrome/chrome"), undefined);
+ });
+
+ test("B1: empty value → undefined", () => {
+ assert.equal(parseUserDataDirFlag("chrome --user-data-dir="), undefined);
+ });
// B2: an explicit --user-data-dir is decisive in both directions
- {
+ test("B2: exact user-data-dir match outranks a flagless process", () => {
const target = "/tmp/pi-e2e-abc";
const match = rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome", explicitUserDataDir: target }, target);
+ const noFlag = rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }, target);
+ assert.ok(match > noFlag);
+ });
+
+ test("B2: a DIFFERENT explicit user-data-dir ranks below a flagless process", () => {
+ const target = "/tmp/pi-e2e-abc";
const other = rankBrowserCandidate(
{ exePath: "/opt/google/chrome/chrome", explicitUserDataDir: "/tmp/somewhere-else" },
target,
);
const noFlag = rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }, target);
- check(match > noFlag, "B2: exact user-data-dir match outranks a flagless process");
- check(other < noFlag, "B2: a DIFFERENT explicit user-data-dir ranks below a flagless process");
- check(other === 0, "B2: a different explicit dir is disqualified outright");
- }
+ assert.ok(other < noFlag);
+ });
+
+ test("B2: a different explicit dir is disqualified outright", () => {
+ const target = "/tmp/pi-e2e-abc";
+ const other = rankBrowserCandidate(
+ { exePath: "/opt/google/chrome/chrome", explicitUserDataDir: "/tmp/somewhere-else" },
+ target,
+ );
+ assert.equal(other, 0);
+ });
// B3: browser family match decides between flagless processes
- {
+ test("B3: Brave wins for a Brave user-data-dir", () => {
const braveDir = join(homedir(), ".config/BraveSoftware/Brave-Browser");
const brave = rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }, braveDir);
const chrome = rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }, braveDir);
- check(brave > chrome, "B3: Brave wins for a Brave user-data-dir");
+ assert.ok(brave > chrome);
+ });
+ test("B3: Chrome wins for a Chrome user-data-dir", () => {
const chromeDir = join(homedir(), ".config/google-chrome");
- check(
+ assert.ok(
rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }, chromeDir) >
rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }, chromeDir),
- "B3: Chrome wins for a Chrome user-data-dir",
);
+ });
+ test("B3: Chromium is not mistaken for Chrome", () => {
const chromiumDir = join(homedir(), ".config/chromium");
- check(
+ assert.ok(
rankBrowserCandidate({ exePath: "/usr/bin/chromium" }, chromiumDir) >
rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }, chromiumDir),
- "B3: Chromium is not mistaken for Chrome",
);
- }
+ });
// B4: no preference expressed → every candidate is equally acceptable
- {
- check(
- rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }) ===
- rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }),
- "B4: without a target dir, candidates rank equally",
+ test("B4: without a target dir, candidates rank equally", () => {
+ assert.equal(
+ rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }),
+ rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome" }),
);
- }
+ });
// B5: Windows path comparison ignores case and separator style
- if (process.platform === "win32") {
- check(
+ test("B5 win: case and separator differences still match", { skip: process.platform !== "win32" }, () => {
+ assert.equal(
rankBrowserCandidate(
- { exePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", explicitUserDataDir: "C:\\Users\\U\\Data" },
+ {
+ exePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
+ explicitUserDataDir: "C:\\Users\\U\\Data",
+ },
"c:/users/u/data",
- ) === 4,
- "B5 win: case and separator differences still match",
+ ),
+ 4,
);
- }
+ });
// B6: Linux's deleted-binary marker. An in-place `google-chrome` upgrade
// unlinks the running binary, after which /proc/<pid>/exe reads
// "…/chrome (deleted)" — a path spawn() rejects with ENOENT.
- {
- check(
- stripDeletedSuffix("/opt/google/chrome/chrome (deleted)") === "/opt/google/chrome/chrome",
- "B6: marker stripped from a replaced binary's path",
- );
- check(stripDeletedSuffix("/opt/google/chrome/chrome") === undefined, "B6: unmarked path left alone");
- check(stripDeletedSuffix(" (deleted)") === undefined, "B6: marker alone is not a path");
- }
+ test("B6: marker stripped from a replaced binary's path", () => {
+ assert.equal(stripDeletedSuffix("/opt/google/chrome/chrome (deleted)"), "/opt/google/chrome/chrome");
+ });
+
+ test("B6: unmarked path left alone", () => {
+ assert.equal(stripDeletedSuffix("/opt/google/chrome/chrome"), undefined);
+ });
+
+ test("B6: marker alone is not a path", () => {
+ assert.equal(stripDeletedSuffix(" (deleted)"), undefined);
+ });
// B7: only a path that is actually on disk may be handed on as spawnable.
- {
- const root = mkdtempSync(join(tmpdir(), "pi-exe-"));
- try {
- const real = join(root, "chrome");
+ describe("B7", () => {
+ let root: string;
+ let real: string;
+ let literal: string;
+
+ before(() => {
+ root = mkdtempSync(join(tmpdir(), "pi-exe-"));
+ real = join(root, "chrome");
writeFileSync(real, "#!/bin/sh\n");
chmodSync(real, 0o755);
+ });
- check(await spawnableExePath(real) === real, "B7: an existing path is returned unchanged");
- check(
- await spawnableExePath(`${real} (deleted)`) === real,
- "B7: a replaced binary resolves to the live install path",
- );
- check(await spawnableExePath(join(root, "absent")) === undefined, "B7: a missing path yields undefined");
- check(await spawnableExePath("") === undefined, "B7: an empty path yields undefined");
-
- // The launch gate must never pass on a path it cannot spawn — that is
- // what produced a 15s "couldn't open a window" timeout instead of a
- // named error.
- check(
- await resolveBrowserExecutable({ running: true, exePath: `${real} (deleted)` }) === real,
- "B7: resolveBrowserExecutable repairs a replaced-binary path",
- );
- if (process.platform !== "win32") {
- check(
- await resolveBrowserExecutable({ running: true, exePath: join(root, "absent") }) === undefined,
- "B7: an unspawnable path is not passed on as usable",
- );
- }
-
- // A binary genuinely named "… (deleted)" must win over the stripped form.
- // Created last, so the checks above see only the ordinary case.
- const literal = join(root, "chrome (deleted)");
- writeFileSync(literal, "#!/bin/sh\n");
- chmodSync(literal, 0o755);
- check(
- await spawnableExePath(literal) === literal,
- "B7: a real file named '… (deleted)' is preferred over stripping",
- );
- } finally {
+ after(() => {
rmSync(root, { recursive: true, force: true });
- }
- }
+ });
+
+ test("B7: an existing path is returned unchanged", async () => {
+ assert.equal(await spawnableExePath(real), real);
+ });
- console.log(`\n${passed} passed, ${failed} failed`);
- process.exit(failed === 0 ? 0 : 1);
-}
+ test("B7: a replaced binary resolves to the live install path", async () => {
+ assert.equal(await spawnableExePath(`${real} (deleted)`), real);
+ });
-void main();
+ test("B7: a missing path yields undefined", async () => {
+ assert.equal(await spawnableExePath(join(root, "absent")), undefined);
+ });
+
+ test("B7: an empty path yields undefined", async () => {
+ assert.equal(await spawnableExePath(""), undefined);
+ });
+
+ // The launch gate must never pass on a path it cannot spawn — that is
+ // what produced a 15s "couldn't open a window" timeout instead of a
+ // named error.
+ test("B7: resolveBrowserExecutable repairs a replaced-binary path", async () => {
+ assert.equal(await resolveBrowserExecutable({ running: true, exePath: `${real} (deleted)` }), real);
+ });
+
+ test(
+ "B7: an unspawnable path is not passed on as usable",
+ { skip: process.platform === "win32" },
+ async () => {
+ assert.equal(await resolveBrowserExecutable({ running: true, exePath: join(root, "absent") }), undefined);
+ },
+ );
+
+ // A binary genuinely named "… (deleted)" must win over the stripped form.
+ // Created last, so the checks above see only the ordinary case.
+ test("B7: a real file named '… (deleted)' is preferred over stripping", async () => {
+ literal = join(root, "chrome (deleted)");
+ writeFileSync(literal, "#!/bin/sh\n");
+ chmodSync(literal, 0o755);
+ assert.equal(await spawnableExePath(literal), literal);
+ });
+ });
+});
diff --git a/test/profile/launch-test.ts b/test/profile/launch-test.ts
index 0061273..70ca8af 100644
--- a/test/profile/launch-test.ts
+++ b/test/profile/launch-test.ts
@@ -7,89 +7,128 @@
* "couldn't open a window in <profile> automatically". These tests pin the
* failure to its real cause instead.
*
- * Run: npx tsx test/profile/launch-test.ts
+ * Run: npm test
*/
+import { test, before, after, describe } from "node:test";
+import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { openProfileWindow } from "../../src/profile/launch";
-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> {
- const root = mkdtempSync(join(tmpdir(), "pi-launch-"));
- try {
- // L1: a nonexistent binary — the shape of an in-place browser upgrade,
- // where /proc/<pid>/exe still names the unlinked file.
- {
- const missing = join(root, "chrome (deleted)");
- const r = await openProfileWindow({ exePath: missing, profileDir: "Default" });
- check(!r.success, "L1: a missing executable fails the launch");
- if (!r.success) {
- check(r.error.includes(missing), "L1: the error names the path that could not be launched");
- check(r.error.includes("ENOENT"), "L1: the error carries spawn's own cause");
- }
- }
+describe("openProfileWindow's failure reporting", () => {
+ let root: string;
+
+ before(() => {
+ root = mkdtempSync(join(tmpdir(), "pi-launch-"));
+ });
+
+ after(() => {
+ rmSync(root, { recursive: true, force: true });
+ });
+
+ // L1: a nonexistent binary — the shape of an in-place browser upgrade,
+ // where /proc/<pid>/exe still names the unlinked file.
+ describe("L1", () => {
+ const missing = (): string => join(root, "chrome (deleted)");
+
+ test("L1: a missing executable fails the launch", async () => {
+ const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
+ assert.ok(!r.success);
+ });
+
+ test("L1: the error names the path that could not be launched", async () => {
+ const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
+ assert.ok(!r.success);
+ assert.ok(r.error.includes(missing()));
+ });
- // L2: a file that exists but is not executable — same async 'error' path,
- // different errno (EACCES). Skipped where the check is meaningless.
- if (process.platform !== "win32" && process.getuid?.() !== 0) {
+ test("L1: the error carries spawn's own cause", async () => {
+ const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
+ assert.ok(!r.success);
+ assert.ok(r.error.includes("ENOENT"));
+ });
+ });
+
+ // L2: a file that exists but is not executable — same async 'error' path,
+ // different errno (EACCES). Skipped where the check is meaningless.
+ test(
+ "L2: a non-executable file fails the launch",
+ { skip: process.platform === "win32" || process.getuid?.() === 0 },
+ async () => {
const notExec = join(root, "not-executable");
writeFileSync(notExec, "#!/bin/sh\n", { mode: 0o644 });
const r = await openProfileWindow({ exePath: notExec, profileDir: "Default" });
- check(!r.success, "L2: a non-executable file fails the launch");
- }
+ assert.ok(!r.success);
+ },
+ );
+
+ // L3: a failed launch leaves no sentinel file behind. The caller only calls
+ // cleanup() on the success path, so the failure path owns its own tidying.
+ describe("L3", () => {
+ let before_: string | undefined;
- // L3: a failed launch leaves no sentinel file behind. The caller only calls
- // cleanup() on the success path, so the failure path owns its own tidying.
- {
- const before = process.env["TMPDIR"];
+ before(() => {
+ before_ = process.env["TMPDIR"];
process.env["TMPDIR"] = root;
- try {
- const r = await openProfileWindow({ exePath: join(root, "absent"), profileDir: "Default" });
- check(!r.success, "L3: launch failed as set up");
- const leftovers = readdirSync(root).filter((n) => n.startsWith("pi-harness-"));
- check(leftovers.length === 0, "L3: no handshake page is left on disk after a failed launch");
- } finally {
- if (before === undefined) delete process.env["TMPDIR"];
- else process.env["TMPDIR"] = before;
- }
- }
+ });
+
+ after(() => {
+ if (before_ === undefined) delete process.env["TMPDIR"];
+ else process.env["TMPDIR"] = before_;
+ });
+
+ test("L3: launch failed as set up", async () => {
+ const r = await openProfileWindow({ exePath: join(root, "absent"), profileDir: "Default" });
+ assert.ok(!r.success);
+ });
+
+ test("L3: no handshake page is left on disk after a failed launch", async () => {
+ await openProfileWindow({ exePath: join(root, "absent"), profileDir: "Default" });
+ const leftovers = readdirSync(root).filter((n) => n.startsWith("pi-harness-"));
+ assert.equal(leftovers.length, 0);
+ });
+ });
- // L4: a launcher that starts succeeds, and the sentinel it advertises is a
- // real, readable file — the page the browser is asked to open.
- {
+ // L4: a launcher that starts succeeds, and the sentinel it advertises is a
+ // real, readable file — the page the browser is asked to open.
+ describe("L4", () => {
+ let r: Awaited<ReturnType<typeof openProfileWindow>>;
+
+ before(async () => {
const stub = join(root, "stub-browser");
writeFileSync(stub, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
- const r = await openProfileWindow({ exePath: stub, profileDir: "Default" });
- check(r.success, "L4: a startable binary yields a handle");
+ r = await openProfileWindow({ exePath: stub, profileDir: "Default" });
+ });
+
+ test("L4: a startable binary yields a handle", () => {
+ assert.ok(r.success);
+ });
+
+ test("L4: the sentinel page exists before the browser is polled", () => {
+ assert.ok(r.success);
+ if (r.success) {
+ const sentinel = fileURLToPath(r.data.sentinelUrl);
+ assert.ok(existsSync(sentinel));
+ }
+ });
+
+ test("L4: cleanup removes the sentinel page", async () => {
+ assert.ok(r.success);
if (r.success) {
const sentinel = fileURLToPath(r.data.sentinelUrl);
- check(existsSync(sentinel), "L4: the sentinel page exists before the browser is polled");
await r.data.cleanup();
- check(!existsSync(sentinel), "L4: cleanup removes the sentinel page");
+ assert.ok(!existsSync(sentinel));
+ }
+ });
+
+ test("L4: kill is safe to call twice", () => {
+ assert.ok(r.success);
+ if (r.success) {
r.data.kill();
r.data.kill();
- check(true, "L4: kill is safe to call twice");
}
- }
- } finally {
- rmSync(root, { recursive: true, force: true });
- }
-
- console.log(`\n${passed} passed, ${failed} failed`);
- process.exit(failed === 0 ? 0 : 1);
-}
-
-void main();
+ });
+ });
+});
diff --git a/test/profile/list-test.ts b/test/profile/list-test.ts
index ca39b73..b4397e3 100644
--- a/test/profile/list-test.ts
+++ b/test/profile/list-test.ts
@@ -4,25 +4,15 @@
* Builds throwaway user-data-dirs on disk so both tiers (Local State and the
* Preferences scan) run against real files.
*
- * Run: npx tsx test/profile/list-test.ts
+ * Run: npm test
*/
+import { test, after, describe } from "node:test";
+import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { formatProfileLabel, listProfiles } from "../../src/profile/list";
-let passed = 0;
-let failed = 0;
-const check = (cond: boolean, label: string): void => {
- if (cond) {
- passed++;
- console.log(` ✓ ${label}`);
- } else {
- failed++;
- console.error(` ✗ ${label}`);
- }
-};
-
const roots: string[] = [];
const makeUdd = (): string => {
const dir = mkdtempSync(join(tmpdir(), "pi-profile-test-"));
@@ -36,9 +26,13 @@ const writeProfileDir = (udd: string, dir: string, prefs?: unknown): void => {
if (prefs !== undefined) writeFileSync(join(udd, dir, "Preferences"), JSON.stringify(prefs), "utf8");
};
-async function main(): Promise<void> {
+describe("profile enumeration", () => {
+ after(() => {
+ for (const root of roots) rmSync(root, { recursive: true, force: true });
+ });
+
// L1: tier 1 — names, emails, ordering, last-used marker
- {
+ const makeL1Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, {
profile: {
@@ -51,28 +45,53 @@ async function main(): Promise<void> {
},
},
});
- const profiles = await listProfiles(udd);
- check(profiles.length === 3, "L1: all three profiles listed");
- check(
- profiles.map((p) => p.dir).join(",") === "Default,Profile 1,Profile 2",
- "L1: profiles_order drives ordering, not object key order",
- );
- check(profiles[1]?.label === "Aman (aman@personal.com)", "L1: label is 'Name (email)'");
- check(profiles[1]?.lastUsed === true, "L1: last_used profile is marked");
- check(profiles[0]?.lastUsed === false, "L1: other profiles are not marked");
- }
+ return udd;
+ };
+
+ test("L1: all three profiles listed", async () => {
+ const profiles = await listProfiles(makeL1Udd());
+ assert.equal(profiles.length, 3);
+ });
+
+ test("L1: profiles_order drives ordering, not object key order", async () => {
+ const profiles = await listProfiles(makeL1Udd());
+ assert.equal(profiles.map((p) => p.dir).join(","), "Default,Profile 1,Profile 2");
+ });
+
+ test("L1: label is 'Name (email)'", async () => {
+ const profiles = await listProfiles(makeL1Udd());
+ assert.equal(profiles[1]?.label, "Aman (aman@personal.com)");
+ });
+
+ test("L1: last_used profile is marked", async () => {
+ const profiles = await listProfiles(makeL1Udd());
+ assert.equal(profiles[1]?.lastUsed, true);
+ });
+
+ test("L1: other profiles are not marked", async () => {
+ const profiles = await listProfiles(makeL1Udd());
+ assert.equal(profiles[0]?.lastUsed, false);
+ });
// L2: profile with no signed-in account falls back to the directory
- {
+ const makeL2Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, { profile: { info_cache: { "Profile 3": { name: "Work" } } } });
- const profiles = await listProfiles(udd);
- check(profiles[0]?.label === "Work (Profile 3)", "L2: no account → 'Name (dir)'");
- check(profiles[0]?.email === undefined, "L2: email stays undefined");
- }
+ return udd;
+ };
+
+ test("L2: no account → 'Name (dir)'", async () => {
+ const profiles = await listProfiles(makeL2Udd());
+ assert.equal(profiles[0]?.label, "Work (Profile 3)");
+ });
+
+ test("L2: email stays undefined", async () => {
+ const profiles = await listProfiles(makeL2Udd());
+ assert.equal(profiles[0]?.email, undefined);
+ });
// L3: gaia_name backfills a missing name; ephemeral profiles are skipped
- {
+ const makeL3Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, {
profile: {
@@ -83,13 +102,21 @@ async function main(): Promise<void> {
},
},
});
- const profiles = await listProfiles(udd);
- check(profiles.length === 1, "L3: ephemeral and System Profile entries are excluded");
- check(profiles[0]?.label === "Aman Kumar (gaia@example.com)", "L3: gaia_name backfills a missing name");
- }
+ return udd;
+ };
+
+ test("L3: ephemeral and System Profile entries are excluded", async () => {
+ const profiles = await listProfiles(makeL3Udd());
+ assert.equal(profiles.length, 1);
+ });
+
+ test("L3: gaia_name backfills a missing name", async () => {
+ const profiles = await listProfiles(makeL3Udd());
+ assert.equal(profiles[0]?.label, "Aman Kumar (gaia@example.com)");
+ });
// L4: unlisted profiles are appended alphabetically after profiles_order
- {
+ test("L4: ordered entries first, remainder alphabetical", async () => {
const udd = makeUdd();
writeLocalState(udd, {
profile: {
@@ -102,14 +129,11 @@ async function main(): Promise<void> {
},
});
const profiles = await listProfiles(udd);
- check(
- profiles.map((p) => p.dir).join(",") === "Profile 9,Default,Profile 3",
- "L4: ordered entries first, remainder alphabetical",
- );
- }
+ assert.equal(profiles.map((p) => p.dir).join(","), "Profile 9,Default,Profile 3");
+ });
// L5: corrupt Local State falls through to the Preferences scan
- {
+ const makeL5Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, "{ this is not json");
writeProfileDir(udd, "Default", {
@@ -117,40 +141,44 @@ async function main(): Promise<void> {
account_info: [{ email: "fallback@example.com" }],
});
writeProfileDir(udd, "Crash Reports"); // no Preferences → not a profile
- const profiles = await listProfiles(udd);
- check(profiles.length === 1, "L5: only directories holding Preferences count as profiles");
- check(profiles[0]?.label === "Fallback Name (fallback@example.com)", "L5: tier 2 reads name + account email");
- }
+ return udd;
+ };
+
+ test("L5: only directories holding Preferences count as profiles", async () => {
+ const profiles = await listProfiles(makeL5Udd());
+ assert.equal(profiles.length, 1);
+ });
+
+ test("L5: tier 2 reads name + account email", async () => {
+ const profiles = await listProfiles(makeL5Udd());
+ assert.equal(profiles[0]?.label, "Fallback Name (fallback@example.com)");
+ });
// L6: missing info_cache also falls through to tier 2
- {
+ test("L6: missing info_cache → Preferences scan", async () => {
const udd = makeUdd();
writeLocalState(udd, { profile: { last_used: "Default" } });
writeProfileDir(udd, "Profile 1", { profile: { name: "Tier Two" } });
const profiles = await listProfiles(udd);
- check(profiles[0]?.label === "Tier Two (Profile 1)", "L6: missing info_cache → Preferences scan");
- }
+ assert.equal(profiles[0]?.label, "Tier Two (Profile 1)");
+ });
// L7: nothing readable → empty list, never a throw
- {
+ test("L7: unreadable user-data-dir yields an empty list", async () => {
const profiles = await listProfiles(join(tmpdir(), "pi-profile-does-not-exist-xyz"));
- check(profiles.length === 0, "L7: unreadable user-data-dir yields an empty list");
- }
+ assert.equal(profiles.length, 0);
+ });
// L8: label formatting is total
- {
- check(formatProfileLabel("A", "a@b.c", "Default") === "A (a@b.c)", "L8: email wins the brackets");
- check(formatProfileLabel("A", undefined, "Profile 7") === "A (Profile 7)", "L8: dir fills in without an email");
- check(formatProfileLabel("A", "", "Profile 7") === "A (Profile 7)", "L8: empty email is treated as absent");
- }
-
- for (const root of roots) rmSync(root, { recursive: true, force: true });
+ test("L8: email wins the brackets", () => {
+ assert.equal(formatProfileLabel("A", "a@b.c", "Default"), "A (a@b.c)");
+ });
- console.log(`\n${passed} passed, ${failed} failed`);
- process.exit(failed === 0 ? 0 : 1);
-}
+ test("L8: dir fills in without an email", () => {
+ assert.equal(formatProfileLabel("A", undefined, "Profile 7"), "A (Profile 7)");
+ });
-main().catch((e) => {
- console.error("profile list test failed:", e);
- process.exit(1);
+ test("L8: empty email is treated as absent", () => {
+ assert.equal(formatProfileLabel("A", "", "Profile 7"), "A (Profile 7)");
+ });
});
diff --git a/test/profile/paths-test.ts b/test/profile/paths-test.ts
index 1a2c937..feaf7ae 100644
--- a/test/profile/paths-test.ts
+++ b/test/profile/paths-test.ts
@@ -4,23 +4,13 @@
* Only the branch for the host platform can be exercised, so each assertion is
* guarded by process.platform; the CI matrix is what gives all three coverage.
*
- * Run: npx tsx test/profile/paths-test.ts
+ * Run: npm test
*/
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
import { homedir } 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}`);
- }
-};
-
/** Re-import with a fresh module identity so env changes are picked up. */
const freshImport = async (): Promise<typeof import("../../src/profile/paths")> =>
import(`../../src/profile/paths?cache=${Math.random()}`);
@@ -37,94 +27,154 @@ const withEnv = async (key: string, value: string | undefined, fn: () => Promise
}
};
-async function main(): Promise<void> {
+describe("path resolution", () => {
// A1: agent dir default and override
- await withEnv("PI_CODING_AGENT_DIR", undefined, async () => {
- const { agentDir, pinFilePath } = await freshImport();
- check(agentDir() === join(homedir(), ".pi", "agent"), "A1: default agent dir is ~/.pi/agent");
- check(pinFilePath() === join(homedir(), ".pi", "agent", "browser-harness.json"), "A1: pin file sits in it");
+ test("A1: default agent dir is ~/.pi/agent", async () => {
+ await withEnv("PI_CODING_AGENT_DIR", undefined, async () => {
+ const { agentDir } = await freshImport();
+ assert.equal(agentDir(), join(homedir(), ".pi", "agent"));
+ });
});
+
+ test("A1: pin file sits in it", async () => {
+ await withEnv("PI_CODING_AGENT_DIR", undefined, async () => {
+ const { pinFilePath } = await freshImport();
+ assert.equal(pinFilePath(), join(homedir(), ".pi", "agent", "browser-harness.json"));
+ });
+ });
+
// pi's own expandTildePath only understands "~/" — never "~\" — so the agent
// dir must resolve identically or the pin lands where pi does not look.
- await withEnv("PI_CODING_AGENT_DIR", "~/custom-agent", async () => {
- check(
- (await freshImport()).agentDir() === `${homedir()}/custom-agent`,
- "A1: '~/' in the agent-dir override expands like pi",
- );
+ test("A1: '~/' in the agent-dir override expands like pi", async () => {
+ await withEnv("PI_CODING_AGENT_DIR", "~/custom-agent", async () => {
+ assert.equal((await freshImport()).agentDir(), `${homedir()}/custom-agent`);
+ });
});
- if (process.platform === "win32") {
+
+ test("A1 win: '~\\' is left alone, matching pi's own resolution", { skip: process.platform !== "win32" }, async () => {
await withEnv("PI_CODING_AGENT_DIR", "~\\custom-agent", async () => {
- check(
- (await freshImport()).agentDir() === "~\\custom-agent",
- "A1 win: '~\\' is left alone, matching pi's own resolution",
- );
+ assert.equal((await freshImport()).agentDir(), "~\\custom-agent");
});
- }
+ });
// A2: platform candidate list
- await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
- const dirs = (await freshImport()).userDataDirCandidates();
- check(dirs.length > 0, "A2: candidates are non-empty on this platform");
- check(new Set(dirs).size === dirs.length, "A2: candidate list is de-duplicated");
-
- if (process.platform === "linux") {
- check(dirs.includes(join(homedir(), ".config/google-chrome")), "A2 linux: Chrome stable dir present");
- check(dirs.includes(join(homedir(), "snap/chromium/common/chromium")), "A2 linux: snap Chromium dir present");
- check(dirs.includes(join(homedir(), ".config/BraveSoftware/Brave-Browser")), "A2 linux: Brave dir present");
- } else if (process.platform === "darwin") {
+ test("A2: candidates are non-empty on this platform", async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.ok(dirs.length > 0);
+ });
+ });
+
+ test("A2: candidate list is de-duplicated", async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.equal(new Set(dirs).size, dirs.length);
+ });
+ });
+
+ test("A2 linux: Chrome stable dir present", { skip: process.platform !== "linux" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.ok(dirs.includes(join(homedir(), ".config/google-chrome")));
+ });
+ });
+
+ test("A2 linux: snap Chromium dir present", { skip: process.platform !== "linux" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.ok(dirs.includes(join(homedir(), "snap/chromium/common/chromium")));
+ });
+ });
+
+ test("A2 linux: Brave dir present", { skip: process.platform !== "linux" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.ok(dirs.includes(join(homedir(), ".config/BraveSoftware/Brave-Browser")));
+ });
+ });
+
+ test("A2 macos: Chrome dir present", { skip: process.platform !== "darwin" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
const support = join(homedir(), "Library", "Application Support");
- check(dirs.includes(join(support, "Google/Chrome")), "A2 macos: Chrome dir present");
- check(dirs.includes(join(support, "BraveSoftware/Brave-Browser")), "A2 macos: Brave dir present");
- check(dirs.includes(join(support, "Microsoft Edge")), "A2 macos: Edge dir present");
- } else if (process.platform === "win32") {
+ assert.ok(dirs.includes(join(support, "Google/Chrome")));
+ });
+ });
+
+ test("A2 macos: Brave dir present", { skip: process.platform !== "darwin" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ const support = join(homedir(), "Library", "Application Support");
+ assert.ok(dirs.includes(join(support, "BraveSoftware/Brave-Browser")));
+ });
+ });
+
+ test("A2 macos: Edge dir present", { skip: process.platform !== "darwin" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ const support = join(homedir(), "Library", "Application Support");
+ assert.ok(dirs.includes(join(support, "Microsoft Edge")));
+ });
+ });
+
+ test("A2 win: Chrome dir derives from %LOCALAPPDATA%", { skip: process.platform !== "win32" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ const lad = process.env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
+ assert.ok(dirs.includes(join(lad, "Google/Chrome/User Data")));
+ });
+ });
+
+ test("A2 win: Edge dir present", { skip: process.platform !== "win32" }, async () => {
+ await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
const lad = process.env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
- check(dirs.includes(join(lad, "Google/Chrome/User Data")), "A2 win: Chrome dir derives from %LOCALAPPDATA%");
- check(dirs.includes(join(lad, "Microsoft/Edge/User Data")), "A2 win: Edge dir present");
- }
+ assert.ok(dirs.includes(join(lad, "Microsoft/Edge/User Data")));
+ });
});
// A3: %LOCALAPPDATA% redirection is honoured (Windows-only behaviour)
- if (process.platform === "win32") {
+ test("A3 win: redirected LOCALAPPDATA is used", { skip: process.platform !== "win32" }, async () => {
await withEnv("LOCALAPPDATA", "D:\\Redirected\\Local", async () => {
const dirs = (await freshImport()).userDataDirCandidates();
- check(
- dirs.some((d) => d.startsWith("D:\\Redirected\\Local") || d.startsWith("D:/Redirected/Local")),
- "A3 win: redirected LOCALAPPDATA is used",
- );
+ assert.ok(dirs.some((d) => d.startsWith("D:\\Redirected\\Local") || d.startsWith("D:/Redirected/Local")));
});
- }
+ });
// A4: $CHROME_USER_DATA_DIR override leads the list. This value is ours to
// interpret, so both tilde separators are accepted.
- await withEnv("CHROME_USER_DATA_DIR", join("~", "my-chrome-data"), async () => {
- const dirs = (await freshImport()).userDataDirCandidates();
- check(dirs[0] === join(homedir(), "my-chrome-data"), "A4: env override is first and tilde-expanded");
+ test("A4: env override is first and tilde-expanded", async () => {
+ await withEnv("CHROME_USER_DATA_DIR", join("~", "my-chrome-data"), async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.equal(dirs[0], join(homedir(), "my-chrome-data"));
+ });
});
- await withEnv("CHROME_USER_DATA_DIR", "~/my-chrome-data", async () => {
- const dirs = (await freshImport()).userDataDirCandidates();
- check(dirs[0] === join(homedir(), "my-chrome-data"), "A4: forward-slash tilde expands on every platform");
+
+ test("A4: forward-slash tilde expands on every platform", async () => {
+ await withEnv("CHROME_USER_DATA_DIR", "~/my-chrome-data", async () => {
+ const dirs = (await freshImport()).userDataDirCandidates();
+ assert.equal(dirs[0], join(homedir(), "my-chrome-data"));
+ });
});
// A5: browser naming
- {
+ test("A5: Chrome recognised", async () => {
const { browserNameForUserDataDir } = await freshImport();
- check(browserNameForUserDataDir("/home/u/.config/google-chrome") === "Chrome", "A5: Chrome recognised");
- check(
- browserNameForUserDataDir("/home/u/.config/BraveSoftware/Brave-Browser") === "Brave",
- "A5: Brave recognised",
- );
- check(
- browserNameForUserDataDir("C:\\Users\\u\\AppData\\Local\\Microsoft\\Edge\\User Data") === "Edge",
- "A5: Edge recognised through Windows separators",
- );
- check(browserNameForUserDataDir("/home/u/.config/chromium") === "Chromium", "A5: Chromium beats Chrome");
- }
+ assert.equal(browserNameForUserDataDir("/home/u/.config/google-chrome"), "Chrome");
+ });
+
+ test("A5: Brave recognised", async () => {
+ const { browserNameForUserDataDir } = await freshImport();
+ assert.equal(browserNameForUserDataDir("/home/u/.config/BraveSoftware/Brave-Browser"), "Brave");
+ });
- console.log(`\n${passed} passed, ${failed} failed`);
- process.exit(failed === 0 ? 0 : 1);
-}
+ test("A5: Edge recognised through Windows separators", async () => {
+ const { browserNameForUserDataDir } = await freshImport();
+ assert.equal(browserNameForUserDataDir("C:\\Users\\u\\AppData\\Local\\Microsoft\\Edge\\User Data"), "Edge");
+ });
-main().catch((e) => {
- console.error("profile paths test failed:", e);
- process.exit(1);
+ test("A5: Chromium beats Chrome", async () => {
+ const { browserNameForUserDataDir } = await freshImport();
+ assert.equal(browserNameForUserDataDir("/home/u/.config/chromium"), "Chromium");
+ });
});