diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-27 00:27:08 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | 15a537a99efba614e6e108722462bcb78ec6c3c4 (patch) | |
| tree | 2ff20907fee8ace7a071470d491fe7808dce6a63 | |
| parent | ce5d808cb4cc3581d85b829656d1d478dc43e3a6 (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.ts | 72 | ||||
| -rw-r--r-- | test/deep-research/readability-test.ts | 91 | ||||
| -rw-r--r-- | test/deep-research/serp-parser-test.ts | 97 | ||||
| -rw-r--r-- | test/profile/browser-process-test.ts | 232 | ||||
| -rw-r--r-- | test/profile/launch-test.ts | 167 | ||||
| -rw-r--r-- | test/profile/list-test.ts | 158 | ||||
| -rw-r--r-- | test/profile/paths-test.ts | 202 |
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"); + }); }); |
