pi-chrome 0.15.49 → 0.15.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable user-facing changes to `pi-chrome`.
4
4
 
5
+ ## 0.15.51 — 2026-09-10
6
+
7
+ - **Fewer Chrome commands.** Removed `/chrome status`; use bare `/chrome` for the quick connection, authorization, and background dashboard plus controls. The dashboard remains lightweight and does not run page probes.
8
+ - **Complete Doctor report.** `/chrome doctor` now includes authorization and background state alongside connection, version, page checks, and troubleshooting hints, even when Chrome is offline or outdated.
9
+ - **Command regressions.** Added tests for command dispatch/completion, lightweight dashboard behavior, Doctor state reporting, and failure paths. `/chrome background status` remains available.
10
+
11
+ ## 0.15.50 — 2026-09-10
12
+
13
+ - **README clarity.** Lead with workflow examples, correct setup ordering and platform-specific onboarding instructions, and clarify privacy guidance. Describe background behavior and the on/off/status controls directly.
14
+ - **Documentation-only release.** Browser behavior and permissions are unchanged. The companion version matches the npm package version.
15
+
5
16
  ## 0.15.49 — 2026-09-10
6
17
 
7
18
  - **Validation scope.** Node regression suites passed. Live browser validation remains incomplete: an input attempt encountered `Input.dispatchMouseEvent: Detached while handling command.`; a subsequent retest was blocked by a disconnected companion. No live-browser pass is claimed for these changes.
package/README.md CHANGED
@@ -1,157 +1,111 @@
1
1
  # pi-chrome
2
2
 
3
- > Let [Pi](https://pi.dev) use your existing signed-in Chrome profile after explicit authorization.
3
+ **Give Pi the Chrome you're already signed into.**
4
4
 
5
- **MIT · 0 runtime deps · loopback-only bridge (`127.0.0.1:17318`) · inspectable unpacked Chrome extension.** Review [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension) before loading. Verify setup with `/chrome doctor`.
5
+ Debug your app, inspect signed-in dashboards, and capture screenshots using your existing Chrome profile—without setting up a separate automation browser.
6
6
 
7
- ```text
8
- You: "Find my open GitHub PR tab, summarize review state, and screenshot failing CI."
9
- Agent: chrome_tab(list) → chrome_snapshot(uid:…) → chrome_screenshot(...)
10
- ✓ 3 reviewers, 1 change requested, CI red on iOS. Saved → .pi/chrome-screenshots/ci.png
11
- You: [keeps coding — agent never asked you to log in]
12
- ```
13
-
14
- `pi-chrome` runs through a small Chrome extension inside the Chrome profile **you already use** — including sites where you're already signed in. Agents can inspect or control Chrome only after you run `/chrome authorize` in current Pi session.
7
+ Built for the [Pi coding agent](https://pi.dev).
15
8
 
16
- ---
9
+ ## What you can do
17
10
 
18
- ## Install
11
+ Try prompts like these after setup:
19
12
 
20
- ```bash
21
- pi install npm:pi-chrome
22
- ```
13
+ | Use case | Ask Pi |
14
+ | --- | --- |
15
+ | **Debug a signed-in app** | “Reproduce the filter bug in my staging app. Inspect captured console and network errors, then save a screenshot.” |
16
+ | **Understand an existing page** | “Find my open dashboard tab and summarize what's on the page. Don't change anything.” |
17
+ | **Create evidence for a PR** | “On my local app, capture the empty, loading, and populated states of this feature for my PR.” |
23
18
 
24
- In Pi:
19
+ Pi gets tools to inspect pages, click, type, fill forms, scroll, upload files, capture screenshots, and inspect captured console logs and `fetch`/`XMLHttpRequest` responses. You describe the task; Pi handles the agent loop.
25
20
 
26
- ```text
27
- /chrome onboard
28
- ```
21
+ **Best fit:** interactive workflows in the Chrome profile you already use. For deterministic CI tests, consider a test framework such as Playwright; for fleets of isolated browsers, consider a hosted browser service. See [more workflows](./docs/EXAMPLES.md) and [browser-tool comparisons](./docs/COMPARISON.md).
29
22
 
30
- This opens `chrome://extensions` and copies bundled extension path. In Chrome Extensions:
23
+ ## Quick start
31
24
 
32
- 1. Turn on **Developer mode**.
33
- 2. Click **Load unpacked**.
34
- 3. Open path field with **Cmd+Shift+G** on macOS or **Ctrl+L** on Windows/Linux.
35
- 4. Paste copied path.
36
- 5. Press Enter.
25
+ **Requirements:** [Pi](https://pi.dev) and Google Chrome. Setup includes a one-time manual installation of the bundled Chrome companion extension.
37
26
 
38
- Reload Pi so installed package loads:
27
+ > **Trust and privacy:** The companion has broad browser permissions and runs in your real Chrome profile. Review [its source](./extensions/chrome-profile-bridge/browser-extension/) before loading it, and authorize only tasks you trust. The browser bridge is local, but page content returned to Pi may be sent to your configured model provider.
39
28
 
40
- ```text
41
- /reload
42
- ```
29
+ ### 1. Install and load the Pi package
43
30
 
44
- Check bridge:
31
+ In your terminal:
45
32
 
46
- ```text
47
- /chrome doctor
33
+ ```bash
34
+ pi install npm:pi-chrome
48
35
  ```
49
36
 
50
- You should see:
37
+ Start Pi with `pi`. If Pi is already running, run `/reload` in that session **before** using the `/chrome` commands.
51
38
 
52
- ```text
53
- ✓ Chrome is connected (...)
54
- ```
39
+ ### 2. Connect Chrome
55
40
 
56
- Authorize current session:
41
+ In Pi:
57
42
 
58
43
  ```text
59
- /chrome authorize
60
- /chrome doctor
44
+ /chrome onboard
61
45
  ```
62
46
 
63
- Second doctor run should show all checks passing.
47
+ The setup dialog shows the companion extension's folder path.
64
48
 
65
- ---
49
+ - **macOS:** after confirmation, Pi opens `chrome://extensions`, reveals the companion folder in Finder, and copies its path to your clipboard.
50
+ - **Windows/Linux:** copy the folder path shown in the dialog and open `chrome://extensions` manually. Automatic desktop opening and clipboard setup are currently macOS-only.
66
51
 
67
- ## What it can do
52
+ In Chrome:
68
53
 
69
- - Read and summarize pages you're already signed into.
70
- - Click, type, fill forms, scroll, drag, tap, and upload files.
71
- - Capture screenshots for bugs, PRs, and demos.
72
- - Inspect console logs and captured `fetch`/`XMLHttpRequest` responses.
73
- - Manage tabs without taking over your active window.
74
-
75
- Tool parameters and gotchas are documented inline in Pi.
76
-
77
- ### Typing into rich editors
78
-
79
- `chrome_type` and `chrome_fill` use one native CDP `Input.insertText` operation for focused contenteditables. This avoids per-character delays for long text and preserves Unicode/newlines. Ordinary inputs and textareas retain individual key events.
80
-
81
- For an editor that needs a `keydown` for every character, pass `perCharacter:true`. Bulk insertion still uses Chrome's input system, but does not emit per-character key events or a clipboard `paste` event. Use `includeSnapshot:true` to verify the result; `chrome_fill` still honors `domFallback:false` when synthetic fallback is unwanted.
82
-
83
- ---
54
+ 1. Turn on **Developer mode**.
55
+ 2. Click **Load unpacked**.
56
+ 3. Select the companion folder shown by `/chrome onboard`. On macOS, press **Cmd+Shift+G** in the folder picker and paste the copied path.
84
57
 
85
- ## Safety model
58
+ ### 3. Authorize and verify
86
59
 
87
- Chrome control is locked by default. Authorize per Pi session:
60
+ In Pi:
88
61
 
89
62
  ```text
90
- /chrome authorize # 15 minutes
91
- /chrome authorize 30m # custom duration
92
- /chrome authorize indefinite
93
- /chrome revoke # lock again
94
- /chrome status
63
+ /chrome authorize
64
+ /chrome doctor
95
65
  ```
96
66
 
97
- Safety properties:
98
-
99
- - Extension runs in your real Chrome profile and has broad tab/scripting permissions. Install only from trusted package source.
100
- - Pi side binds to `127.0.0.1:17318` only; no default network exposure.
101
- - Bridge rejects browser-origin command requests, so ordinary web pages cannot drive it through CORS.
102
- - Each Pi session gets its own automation target; user tabs/windows are not closed by cleanup.
103
- - `/chrome revoke` closes only calling session's automation target.
104
-
105
- Security details: [`SECURITY.md`](./SECURITY.md). Architecture details: [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
67
+ Approve the authorization prompt for a task you trust. The default authorization lasts **15 minutes**. Doctor should report `✓ Chrome is connected (...)`; follow its instructions if any checks fail.
106
68
 
107
- ---
108
-
109
- ## Commands
69
+ Then try a read-only first task:
110
70
 
111
71
  ```text
112
- /chrome onboard # guided setup
113
- /chrome doctor # connectivity + version + eval checks
114
- /chrome status # connection + auth + background state
115
- /chrome authorize [duration]
116
- /chrome revoke
117
- /chrome background on # default: hard background policy
118
- /chrome background off # foreground/watch mode
119
- /chrome background status
72
+ List my open Chrome tabs without navigating, clicking, or changing anything.
120
73
  ```
121
74
 
122
- If loaded extension is older than installed `pi-chrome`, `/chrome doctor` tells you to reload it from `chrome://extensions`.
123
-
124
- ### Background policy
125
-
126
- `/chrome background on` is enforced, not an overridable default. Per-call `background:false` cannot bring Chrome forward, new tabs stay inactive, and `chrome_tab activate` is blocked. Use the existing `/chrome background off` for foreground/watch mode; per-call `background:true` still works when that mode is off.
127
-
128
- Screenshots use CDP without activating background tabs. Debugger/capture failures return errors, never an activation fallback. Reload both Pi and the Chrome companion after upgrading; old companions reject background tab creation/screenshots rather than silently switching tabs.
75
+ Run `/chrome revoke` when finished. Use `/chrome authorize` again whenever you want to grant access for another task or session.
129
76
 
130
- This prevents explicit pi-chrome focus/activation, not every Chrome/OS side effect. Trusted input, page popups, native prompts, debugger banners, and macOS Spaces can still affect focus. Inactive pages may throttle rendering or reject focus-gated actions. See [scope and risks](./docs/ARCHITECTURE.md#scope-and-risks).
77
+ ## Safety
131
78
 
132
- ---
79
+ - **Per-session approval.** Pi's Chrome tools require `/chrome authorize`. `/chrome revoke` locks them and requests cleanup of that session's owned automation tabs. Cleanup preserves existing user tabs.
80
+ - **Separate targets by default.** Page actions without an explicit target use a session-owned automation window or tab. The agent can deliberately target an existing tab when your task calls for it.
81
+ - **Local transport, not a sandbox.** The bridge binds to `127.0.0.1:17318` and rejects browser-origin command requests. It does not authenticate arbitrary non-browser local callers; it is not protection against hostile processes on your machine.
82
+ - **Background mode:** `/chrome background on` (default) blocks pi-chrome tools from directly bringing Chrome to the front or switching your selected tab. Use `/chrome background off` to allow those actions, and `/chrome background status` to check the setting.
133
83
 
134
- ## Limits
84
+ ### Limits
135
85
 
136
- `pi-chrome` works best on web-page workflows exposed through DOM, screenshots, tabs, network, console, and Chrome input. It is not full OS automation.
86
+ This is browser automation, not full OS control. Native Chrome/OS dialogs, password-manager prompts, passkeys/security keys/biometrics, CAPTCHA challenges, cross-origin iframe DOM access, rich multitouch/stylus gestures, and arbitrary desktop apps are outside its reliable tool surface. Some workflows need human assistance.
137
87
 
138
- Current limits include native Chrome/OS surfaces, print/save dialogs, permission bubbles, password-manager prompts, cross-origin iframe DOM access, CAPTCHA/bot challenges, passkeys/security keys/biometrics, rich multitouch/pinch/stylus gestures, and arbitrary desktop apps.
88
+ If page inspection or evaluation is blocked, use screenshots and coordinate input where possible. Background pages can throttle rendering or reject focus-gated actions. See the [FAQ](./docs/FAQ.md) for details.
139
89
 
140
- For strict-CSP pages, use screenshots + coordinate input when snapshot/evaluate paths are blocked.
141
-
142
- ---
90
+ ## Commands
143
91
 
144
- ## Docs
92
+ ```text
93
+ /chrome # quick connection/auth/background dashboard and controls
94
+ /chrome onboard # one-time companion setup
95
+ /chrome authorize # authorize this Pi session for 15 minutes
96
+ /chrome authorize 30m # choose a duration
97
+ /chrome authorize indefinite # no time limit; revoke when finished
98
+ /chrome revoke # lock tools and request session cleanup
99
+ /chrome doctor # full diagnostics, including authorization/background state
100
+ /chrome background on # default: block explicit focus/tab activation
101
+ /chrome background off # allow foreground/watch mode
102
+ /chrome background status
103
+ ```
145
104
 
146
- - Examples: [`docs/EXAMPLES.md`](./docs/EXAMPLES.md)
147
- - FAQ: [`docs/FAQ.md`](./docs/FAQ.md)
148
- - Comparison: [`docs/COMPARISON.md`](./docs/COMPARISON.md)
149
- - Security: [`SECURITY.md`](./SECURITY.md)
150
- - Benchmark suite: [`test-suite/README.md`](./test-suite/README.md)
151
- - Architecture: [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)
105
+ Bare `/chrome` checks the connection without running page probes. Use `/chrome doctor` for version and page checks, troubleshooting hints, and authorization/background state.
152
106
 
153
- ---
107
+ Tool parameters are documented inline in Pi. See [architecture](./docs/ARCHITECTURE.md) for target ownership, screenshot behavior, and background-policy details.
154
108
 
155
- ## License
109
+ ### Updating and troubleshooting
156
110
 
157
- MIT. See [LICENSE](./LICENSE).
111
+ After `pi update npm:pi-chrome`, run `/reload` in Pi and reload **Pi Chrome Connector** in `chrome://extensions`. Run `/chrome doctor` to check the connection and companion version.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.49",
4
+ "version": "0.15.51",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -992,7 +992,11 @@ Usage rules:
992
992
  // Shared handlers, dispatched by the unified /chrome command below.
993
993
  const doctorHandler = async (ctx: ExtensionContext) => {
994
994
  ctx.ui.notify("Checking pi-chrome…", "info");
995
- const lines: string[] = [`pi-chrome v${PI_CHROME_VERSION}`];
995
+ const lines: string[] = [
996
+ `pi-chrome v${PI_CHROME_VERSION}`,
997
+ `• Authorization: ${authSummary()}.`,
998
+ `• Background: ${backgroundEnabled ? "on (hard)" : "off (foreground/watch mode)"}.`,
999
+ ];
996
1000
  const status = bridge.status();
997
1001
  const roleLabel = status.mode === "client" ? "sharing another pi session's connection" : "running the Chrome connection for this machine";
998
1002
  lines.push(`• This pi session is ${roleLabel}.`);
@@ -1144,8 +1148,7 @@ Usage rules:
1144
1148
  );
1145
1149
  };
1146
1150
 
1147
- // One-line snapshot of pi-chrome's current state. Used as a header in the bare-/chrome
1148
- // picker and as the body of /chrome status.
1151
+ // Lightweight connection/auth/background header for the bare-/chrome picker. No page probes.
1149
1152
  const statusSummary = async (): Promise<string> => {
1150
1153
  const parts: string[] = [];
1151
1154
  try {
@@ -1163,11 +1166,6 @@ Usage rules:
1163
1166
  return parts.join(" · ");
1164
1167
  };
1165
1168
 
1166
- const statusHandler = async (ctx: ExtensionContext) => {
1167
- ctx.ui.notify("Checking Chrome connection…", "info");
1168
- ctx.ui.notify(await statusSummary(), "info");
1169
- };
1170
-
1171
1169
  const openAuthorizeMenu = async (ctx: ExtensionContext): Promise<void> => {
1172
1170
  while (true) {
1173
1171
  const choice = await ctx.ui.select("Authorize Chrome control", [
@@ -1225,7 +1223,7 @@ Usage rules:
1225
1223
 
1226
1224
  pi.registerCommand("chrome", {
1227
1225
  description:
1228
- "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome statusone-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — enforce no explicit focus/tab activation, or allow foreground/watch mode.\nRun with no arguments for an interactive picker that shows current state.",
1226
+ "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome doctorfull health check plus authorization and background state.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — enforce no explicit focus/tab activation, or allow foreground/watch mode.\nRun with no arguments for an interactive picker that shows current state.",
1229
1227
  getArgumentCompletions: (prefix) => {
1230
1228
  const raw = prefix;
1231
1229
  const trimmedRight = raw.replace(/\s+$/, "");
@@ -1244,8 +1242,7 @@ Usage rules:
1244
1242
  candidates = [
1245
1243
  { fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
1246
1244
  { fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
1247
- { fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
1248
- { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
1245
+ { fullValue: "doctor", label: "doctor", description: "Full diagnostics: connection, version, page checks, authorization, and background state." },
1249
1246
  { fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
1250
1247
  { fullValue: "background", label: "background", description: "Enforce hard background or allow foreground/watch mode." },
1251
1248
  ];
@@ -1279,7 +1276,6 @@ Usage rules:
1279
1276
  switch (head) {
1280
1277
  case "authorize": return authorizeHandler(ctx, subArgs);
1281
1278
  case "revoke": return revokeHandler(ctx);
1282
- case "status": return statusHandler(ctx);
1283
1279
  case "doctor": return doctorHandler(ctx);
1284
1280
  case "onboard": return onboardHandler(ctx);
1285
1281
  case "background":
@@ -1292,7 +1288,7 @@ Usage rules:
1292
1288
  return;
1293
1289
  }
1294
1290
  default:
1295
- ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`, "warning");
1291
+ ctx.ui.notify(`Unknown subcommand '${head}'. Run /chrome for current state and controls, or try: /chrome authorize | revoke | doctor | onboard | background.`, "warning");
1296
1292
  }
1297
1293
  },
1298
1294
  });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.49",
3
+ "version": "0.15.51",
4
4
  "scripts": {
5
- "test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && node test-suite/unit/session-cleanup.test.mjs && node test-suite/unit/background-policy.test.mjs && node test-suite/unit/input-reliability.test.mjs",
5
+ "test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && node test-suite/unit/session-cleanup.test.mjs && node test-suite/unit/background-policy.test.mjs && node test-suite/unit/input-reliability.test.mjs && node test-suite/unit/chrome-command.test.mjs",
6
6
  "version": "node scripts/sync-manifest-version.js",
7
7
  "prepublishOnly": "node scripts/sync-manifest-version.js"
8
8
  },
@@ -0,0 +1,163 @@
1
+ // Exercise the shipped /chrome command registration and handlers without opening Chrome or a bridge.
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import vm from "node:vm";
5
+ import { stripTypeScriptTypes } from "node:module";
6
+ import { test } from "node:test";
7
+
8
+ const source = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8");
9
+ const { version } = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
10
+ const now = 1_000_000;
11
+ function section(start, end) {
12
+ const from = source.indexOf(start);
13
+ const to = source.indexOf(end, from);
14
+ assert.ok(from >= 0 && to > from, `Missing source section: ${start}`);
15
+ return source.slice(from, to);
16
+ }
17
+ const commandSource = stripTypeScriptTypes([
18
+ section("const authSummary =", "\n\tconst chromeControlAuthorized ="),
19
+ section("// Shared handlers,", "\n\tfunction registerChromeTools("),
20
+ ].join("\n"));
21
+
22
+ function healthyResponse(action) {
23
+ switch (action) {
24
+ case "tab.version": return { extensionVersion: version };
25
+ case "page.evaluate": return 2;
26
+ case "page.probe": return { arithmetic: 2, location: "https://fixture.test/", webdriver: false };
27
+ default: throw new Error(`Unexpected bridge action: ${action}`);
28
+ }
29
+ }
30
+
31
+ function harness({ until, background = true, mode = "server", choices = [], send = healthyResponse } = {}) {
32
+ const calls = [], notices = [], menus = [];
33
+ let command;
34
+ const ctx = {
35
+ ui: {
36
+ notify: (...args) => notices.push(args),
37
+ async select(title, items) {
38
+ menus.push({ title, items: Array.from(items) });
39
+ return choices[menus.length - 1];
40
+ },
41
+ },
42
+ };
43
+ const sandbox = {
44
+ Date: { now: () => now }, PI_CHROME_VERSION: version,
45
+ chromeAuthorizedUntil: until, backgroundEnabled: background,
46
+ hostnameOf: (url) => new URL(url).hostname,
47
+ bridge: {
48
+ status: () => ({ mode }),
49
+ async send(action, params, timeout) {
50
+ calls.push({ action, params: JSON.parse(JSON.stringify(params)), timeout });
51
+ return send(action, params, timeout);
52
+ },
53
+ },
54
+ pi: { registerCommand(name, definition) { assert.equal(name, "chrome"); command = definition; } },
55
+ };
56
+ vm.runInNewContext(commandSource, sandbox);
57
+ return { command, calls, notices, menus, sandbox, run: (args = "") => command.handler(args, ctx) };
58
+ }
59
+
60
+ test("command help and root completion omit status; nested background status remains available", () => {
61
+ const h = harness();
62
+ assert.doesNotMatch(h.command.description, /\/chrome status\b/);
63
+ assert.match(h.command.description, /\/chrome doctor/);
64
+ assert.deepEqual(Array.from(h.command.getArgumentCompletions(""), (item) => item.value), [
65
+ "authorize", "revoke", "doctor", "onboard", "background",
66
+ ]);
67
+ assert.equal(h.command.getArgumentCompletions("sta"), null);
68
+ assert.equal(h.command.getArgumentCompletions("doctor")[0].value, "doctor");
69
+ assert.equal(h.command.getArgumentCompletions("background st")[0].value, "background status");
70
+ assert.equal(h.command.getArgumentCompletions("authorize 15")[0].value, "authorize 15m");
71
+ });
72
+
73
+ test("removed status command returns a warning without probing Chrome", async () => {
74
+ const h = harness();
75
+ await h.run("status");
76
+ assert.equal(h.calls.length, 0);
77
+ assert.equal(h.notices.length, 1);
78
+ assert.match(h.notices[0][0], /Unknown subcommand 'status'/);
79
+ assert.doesNotMatch(h.notices[0][0], /\| status \|/);
80
+ assert.equal(h.notices[0][1], "warning");
81
+ });
82
+
83
+ test("bare chrome shows loading immediately, then a lightweight dashboard without page probes", async () => {
84
+ let finish;
85
+ const h = harness({ send: () => new Promise((resolve) => { finish = resolve; }) });
86
+ const done = h.run();
87
+ assert.deepEqual(h.notices, [["Checking Chrome connection…", "info"]]);
88
+ assert.equal(h.menus.length, 0);
89
+ assert.deepEqual(h.calls, [{ action: "tab.version", params: {}, timeout: 5_000 }]);
90
+ finish({ extensionVersion: version });
91
+ await done;
92
+ assert.match(h.menus[0].title, /Chrome connected.*auth: locked.*background: on \(hard\)/);
93
+ assert.ok(h.menus[0].items.includes("Doctor / troubleshoot"));
94
+ assert.ok(!h.menus[0].items.some((item) => /status/i.test(item)));
95
+ assert.equal(h.calls.length, 1);
96
+ });
97
+
98
+ test("dashboard retains authorization/background state when Chrome is offline or outdated", async () => {
99
+ for (const [send, expected] of [
100
+ [() => { throw new Error("offline"); }, /Chrome not responding/],
101
+ [() => ({ extensionVersion: "0.0.0" }), /Chrome extension v0\.0\.0.*reload extension/],
102
+ ]) {
103
+ const h = harness({ until: "indefinite", send });
104
+ await h.run("background off");
105
+ await h.run("background status");
106
+ assert.match(h.notices.at(-1)[0], /background is off/);
107
+ await h.run();
108
+ assert.match(h.menus[0].title, expected);
109
+ assert.match(h.menus[0].title, /auth: authorized indefinitely.*background: off/);
110
+ assert.deepEqual(h.calls, [{ action: "tab.version", params: {}, timeout: 5_000 }]);
111
+ }
112
+ });
113
+
114
+ test("Doctor includes locked, timed, indefinite, and expired authorization plus background state", async () => {
115
+ for (const [until, expected] of [
116
+ [undefined, "locked"], [now + 15 * 60_000, "authorized for ~15m"],
117
+ ["indefinite", "authorized indefinitely"], [now, "locked"],
118
+ ]) {
119
+ for (const background of [true, false]) {
120
+ const h = harness({ until, background });
121
+ await h.run("doctor");
122
+ assert.equal(h.notices[0][0], "Checking pi-chrome…");
123
+ const report = h.notices.at(-1)[0];
124
+ assert.ok(report.includes(`pi-chrome v${version}`));
125
+ assert.ok(report.includes(`Authorization: ${expected}`));
126
+ assert.ok(report.includes(`Background: ${background ? "on (hard)" : "off"}`));
127
+ assert.match(report, /Chrome is connected/);
128
+ assert.match(report, /can run code/);
129
+ assert.match(report, /fixture\.test/);
130
+ assert.deepEqual(h.calls.map(({ action, timeout }) => [action, timeout]), [
131
+ ["tab.version", 35_000], ["page.evaluate", 10_000], ["page.probe", 10_000],
132
+ ]);
133
+ assert.ok(h.calls.filter((call) => call.action.startsWith("page.")).every((call) => call.params.foreground === false));
134
+ assert.equal(h.sandbox.chromeAuthorizedUntil, until, "diagnostics do not grant or change authorization");
135
+ assert.equal(h.sandbox.backgroundEnabled, background);
136
+ }
137
+ }
138
+ });
139
+
140
+ test("Doctor retains local state and repair hints when connection/version checks fail", async () => {
141
+ for (const [send, expected] of [
142
+ [() => { throw new Error("offline"); }, /Chrome isn't responding: offline/],
143
+ [() => ({ extensionVersion: "0.0.0" }), /old version \(0\.0\.0\)/],
144
+ ]) {
145
+ const h = harness({ until: "indefinite", background: false, mode: "client", send });
146
+ await h.run("doctor");
147
+ const report = h.notices.at(-1)[0];
148
+ assert.match(report, /Authorization: authorized indefinitely/);
149
+ assert.match(report, /Background: off/);
150
+ assert.match(report, /sharing another pi session's connection/);
151
+ assert.match(report, expected);
152
+ assert.match(report, /Fix:/);
153
+ assert.deepEqual(h.calls.map((call) => call.action), ["tab.version"]);
154
+ }
155
+ });
156
+
157
+ test("choosing Doctor explicitly from the dashboard runs full diagnostics", async () => {
158
+ const h = harness({ choices: ["Doctor / troubleshoot"] });
159
+ await h.run();
160
+ assert.deepEqual(h.calls.map((call) => call.action), ["tab.version", "tab.version", "page.evaluate", "page.probe"]);
161
+ assert.match(h.notices.at(-1)[0], /Authorization: locked/);
162
+ assert.match(h.notices.at(-1)[0], /Background: on \(hard\)/);
163
+ });