@firstpick/pi-package-webui 0.4.0 → 0.4.2

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.
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
- import { chmod, mkdtemp, rm, stat } from "node:fs/promises";
3
+ import { chmod, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { networkInterfaces, tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { setTimeout as delay } from "node:timers/promises";
@@ -154,6 +154,47 @@ try {
154
154
  assert.equal(gitRemote.status, 200);
155
155
  assert.equal(gitRemote.body?.ok, true, "remote endpoint should add origin without pushing");
156
156
  assert.equal(gitRemote.body?.data?.remoteUrl, "https://github.com/Firstp1ck/pi-webui-http-harness.git");
157
+
158
+ await writeFile(path.join(cwd, "single.txt"), "created\n");
159
+ const gitAddCreated = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
160
+ assert.equal(gitAddCreated.status, 200);
161
+ assert.equal(gitAddCreated.body?.ok, true, "git add endpoint should stage a new single file");
162
+ const createdDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
163
+ assert.equal(createdDefault.status, 200);
164
+ assert.equal(createdDefault.body?.ok, true, "default commit message endpoint should return ok for a staged single file");
165
+ assert.equal(createdDefault.body?.data?.message, "created single.txt");
166
+ const createdCommit = await request("127.0.0.1", "/api/git-workflow/commit", { method: "POST", body: { variant: "input", message: createdDefault.body?.data?.message, tab: tabId } });
167
+ assert.equal(createdCommit.status, 200);
168
+ assert.equal(createdCommit.body?.ok, true, "input commit endpoint should accept the generated single-file default");
169
+
170
+ await writeFile(path.join(cwd, "single.txt"), "updated\n");
171
+ const gitAddUpdated = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
172
+ assert.equal(gitAddUpdated.status, 200);
173
+ assert.equal(gitAddUpdated.body?.ok, true, "git add endpoint should stage a single-file update");
174
+ const updatedDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
175
+ assert.equal(updatedDefault.status, 200);
176
+ assert.equal(updatedDefault.body?.data?.message, "updated single.txt");
177
+ const updatedCommit = await request("127.0.0.1", "/api/git-workflow/commit", { method: "POST", body: { variant: "input", message: updatedDefault.body?.data?.message, tab: tabId } });
178
+ assert.equal(updatedCommit.status, 200);
179
+ assert.equal(updatedCommit.body?.ok, true, "input commit endpoint should accept the update default");
180
+
181
+ await rm(path.join(cwd, "single.txt"));
182
+ const gitAddDeleted = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
183
+ assert.equal(gitAddDeleted.status, 200);
184
+ assert.equal(gitAddDeleted.body?.ok, true, "git add endpoint should stage a single-file deletion");
185
+ const deletedDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
186
+ assert.equal(deletedDefault.status, 200);
187
+ assert.equal(deletedDefault.body?.data?.message, "deleted single.txt");
188
+
189
+ await writeFile(path.join(cwd, "multi-a.txt"), "a\n");
190
+ await writeFile(path.join(cwd, "multi-b.txt"), "b\n");
191
+ const gitAddMultiple = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
192
+ assert.equal(gitAddMultiple.status, 200);
193
+ assert.equal(gitAddMultiple.body?.ok, true, "git add endpoint should stage multiple files");
194
+ const multipleDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
195
+ assert.equal(multipleDefault.status, 200);
196
+ assert.equal(multipleDefault.body?.ok, true, "default commit message endpoint should still return ok when no default is available");
197
+ assert.equal(multipleDefault.body?.data?.message, "", "multiple staged files should not get a default commit message");
157
198
  } else {
158
199
  console.log("http-endpoints-harness: git not available; skipping git init workflow endpoint checks");
159
200
  }
@@ -204,8 +245,15 @@ try {
204
245
  assert.equal(traversalDelete.status, 403, "session delete outside the session dir must return 403");
205
246
  assert.match(String(traversalDelete.body?.error || ""), /session directory/i);
206
247
 
248
+ const initialAuth = await request("127.0.0.1", "/api/remote-auth");
249
+ assert.equal(initialAuth.status, 200);
250
+ assert.equal(initialAuth.body?.data?.auth?.enabled, false, "remote PIN auth should be off by default");
251
+
207
252
  const lan = lanAddress();
208
253
  if (lan) {
254
+ const remoteHealthBeforeAuth = await request(lan, "/api/health");
255
+ assert.equal(remoteHealthBeforeAuth.status, 200, "LAN clients should connect without a PIN while auth is off");
256
+
209
257
  const remoteDelete = await request(lan, "/api/session-delete", {
210
258
  method: "POST",
211
259
  body: { sessionPath: path.join(cwd, "outside.jsonl"), confirmed: true, tab: tabId },
@@ -221,7 +269,55 @@ try {
221
269
 
222
270
  const remoteClose = await request(lan, "/api/network/close", { method: "POST" });
223
271
  assert.equal(remoteClose.status, 403, "network close must be localhost-only");
272
+
273
+ const enableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: true } });
274
+ assert.equal(enableAuth.status, 200, "localhost can enable remote PIN auth");
275
+ const pin = enableAuth.body?.data?.auth?.pin;
276
+ assert.match(pin, /^\d{4}$/, "enabling remote auth should generate a 4-digit PIN");
277
+
278
+ const remoteHealthWithAuth = await request(lan, "/api/health");
279
+ assert.equal(remoteHealthWithAuth.status, 401, "unauthenticated LAN clients should be challenged while remote auth is on");
280
+
281
+ const wrongPin = pin === "0000" ? "0001" : "0000";
282
+ const badLogin = await request(lan, "/api/remote-auth", { method: "POST", body: { pin: wrongPin } });
283
+ assert.equal(badLogin.status, 403, "wrong remote PIN should be rejected");
284
+
285
+ const loginResponse = await fetch(`http://${lan}:${port}/api/remote-auth`, {
286
+ method: "POST",
287
+ headers: { "content-type": "application/json" },
288
+ body: JSON.stringify({ pin }),
289
+ signal: AbortSignal.timeout(5_000),
290
+ });
291
+ assert.equal(loginResponse.status, 200, "correct remote PIN should be accepted");
292
+ const authCookie = loginResponse.headers.get("set-cookie")?.split(";", 1)[0];
293
+ assert.ok(authCookie, "remote auth login should set an auth cookie");
294
+
295
+ const authedHealth = await fetch(`http://${lan}:${port}/api/health`, {
296
+ headers: { cookie: authCookie },
297
+ signal: AbortSignal.timeout(5_000),
298
+ });
299
+ assert.equal(authedHealth.status, 200, "authenticated LAN client should reach guarded APIs");
300
+ await authedHealth.json();
301
+
302
+ const remoteSettings = await fetch(`http://${lan}:${port}/api/remote-auth/settings`, {
303
+ method: "POST",
304
+ headers: { "content-type": "application/json", cookie: authCookie },
305
+ body: JSON.stringify({ enabled: false }),
306
+ signal: AbortSignal.timeout(5_000),
307
+ });
308
+ assert.equal(remoteSettings.status, 403, "remote clients must not toggle remote PIN auth settings");
309
+ await remoteSettings.json().catch(() => undefined);
310
+
311
+ const disableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: false } });
312
+ assert.equal(disableAuth.status, 200, "localhost can disable remote PIN auth");
313
+ const remoteHealthAfterDisable = await request(lan, "/api/health");
314
+ assert.equal(remoteHealthAfterDisable.status, 200, "LAN clients should reconnect without a PIN after auth is disabled");
224
315
  } else {
316
+ const enableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: true } });
317
+ assert.equal(enableAuth.status, 200, "localhost can enable remote PIN auth");
318
+ assert.match(enableAuth.body?.data?.auth?.pin, /^\d{4}$/);
319
+ const disableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: false } });
320
+ assert.equal(disableAuth.status, 200, "localhost can disable remote PIN auth");
225
321
  console.log("http-endpoints-harness: no LAN address detected; skipping remote-client checks");
226
322
  }
227
323
 
@@ -267,7 +267,7 @@ assert.match(css, /\.composer-publish-menu:hover > \.composer-publish-button\[da
267
267
  assert.match(css, /\.composer-publish-menu-panel \{[\s\S]*?display:\s*none;[\s\S]*?flex-direction:\s*column/, "Publish workflow menu should hide when closed and expand like grouped tabs");
268
268
  assert.match(css, /\.composer-publish-menu:hover \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu:focus-within \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu\.open \.composer-publish-menu-panel \{\n\s+display:\s*flex;/, "Publish workflow menu should open on hover, focus, or explicit open state");
269
269
  assert.match(css, /\.composer-native-command-button \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu should have a distinct slash-command button style");
270
- assert.match(css, /\.composer-options-menu-panel \{[\s\S]*?max-height:\s*min\(88vh, 36rem\)/, "Options menu should be tall enough for common commands without scrolling on normal viewports");
270
+ assert.match(css, /\.composer-options-menu-panel \{[\s\S]*?max-height:\s*min\(calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\), 44rem\)/, "Options menu should be tall enough for common commands without scrolling on normal viewports");
271
271
  assert.match(css, /\.composer-native-command-menu-item \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu items should be styled separately from publish actions");
272
272
  assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish and command menu buttons should fit beside Git workflow in mobile actions");
273
273
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
@@ -399,6 +399,8 @@ assert.match(app, /initializeCustomBackground\(\)\.catch/, "startup should resto
399
399
  assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
400
400
  assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
401
401
  assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
402
+ assert.match(app, /remoteAuthToggle: \$\("#remoteAuthToggle"\)/, "Controls should expose the remote PIN auth toggle");
403
+ assert.match(app, /api\("\/api\/remote-auth\/settings", \{ method: "POST"/, "remote PIN auth toggle should call the settings endpoint");
402
404
  assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
403
405
  assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
404
406
  assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
@@ -561,7 +563,10 @@ assert.match(app, /\["skills", "tuiSkillsCommand"\][\s\S]*\["tools", "tuiToolsCo
561
563
  assert.match(app, /function setNativeCommandMenuOpen\(open\)/, "frontend should track the skills/tools command menu open state separately from Publish");
562
564
  assert.match(app, /nativeSkillsButton\.hidden = !isOptionalFeatureEnabled\("tuiSkillsCommand"\)[\s\S]*nativeToolsButton\.hidden = !isOptionalFeatureEnabled\("tuiToolsCommand"\)/, "skills/tools menu items should be hidden by their optional feature toggles");
563
565
  assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
564
- assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
566
+ assert.match(app, /function installOptionalFeature\(featureId, \{ update = false \} = \{\}\)/, "optional features should expose install and update actions");
567
+ assert.match(app, /api\("\/api\/optional-features"/, "optional feature panel should fetch package install/update status from the backend");
568
+ assert.match(app, /packageStatus\?\.updateAvailable[\s\S]*action\.textContent = "Update…"/, "optional feature package drift should turn the install action into an update action");
569
+ assert.match(app, /optionalFeatureInstallMessages\.set\(featureId[\s\S]*waiting for package-manager output/, "optional feature installs should show running feedback while npm is active");
565
570
  assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
566
571
  assert.match(app, /id: "safetyGuard"[\s\S]*?@firstpick\/pi-extension-safety-guard/, "optional features should include the safety guard companion");
567
572
  assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-skills/, "optional features should include the TUI skills command companion");
@@ -858,7 +863,7 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu:hover::aft
858
863
  assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-panel \{[\s\S]*?position:\s*absolute;[\s\S]*?inset:\s*auto auto calc\(100% \+ 0\.38rem\) 0;[\s\S]*?max-height:\s*min\(34dvh, 18rem\);[\s\S]*?overflow:\s*auto;/, "opened mobile Actions dropdown panels should float upward over the Actions controls with their own scrollbar");
859
864
  assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-panel \{[\s\S]*?width:\s*100%;[\s\S]*?min-width:\s*0;[\s\S]*?max-width:\s*100%;/, "mobile Actions dropdown panels should align to the width of their trigger buttons");
860
865
  assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-item \{[\s\S]*?width:\s*100%;[\s\S]*?min-width:\s*0;[\s\S]*?white-space:\s*normal;/, "mobile Actions dropdown option buttons should not keep desktop min-widths that misalign with triggers");
861
- assert.match(css, /\.composer-actions-panel > \.composer-options-menu \.composer-publish-menu-panel \{[\s\S]*?inset-inline:\s*auto 0;[\s\S]*?max-height:\s*min\(76dvh, 34rem\);/, "mobile Options dropdown should be tall enough to avoid scrolling for the standard option list");
866
+ assert.match(css, /\.composer-actions-panel > \.composer-options-menu \.composer-publish-menu-panel \{[\s\S]*?inset-inline:\s*auto 0;[\s\S]*?max-height:\s*min\(calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\), 44rem\);/, "mobile Options dropdown should be tall enough to avoid scrolling for the standard option list");
862
867
  assert.match(app, /function setMobileTabsExpanded\(/, "mobile tab strip should be JS-toggleable");
863
868
  assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\) \{[\s\S]*?\.terminal-tab-group \{\n\s+display:\s*grid;\n\s+grid-template-columns:\s*minmax\(0, 1fr\) auto;/, "mobile terminal tab groups should use a stable grid row for the tab and close button when expanded");
864
869
  assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\) \{[\s\S]*?\.terminal-tab-group-menu \{[\s\S]*?grid-column:\s*1 \/ -1;[\s\S]*?margin:\s*0\.34rem 0 0;/, "mobile terminal tab group menus should not add horizontal margins that overflow and distort the tab card");
@@ -956,7 +961,7 @@ assert.match(server, /WEBUI_CONTROLLED_PACKAGES = new Set\(\[WEBUI_PACKAGE, \.\.
956
961
  assert.match(server, /const args = \["--mode", "rpc", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes"\]/, "Web UI tabs should disable implicit resource loading before adding curated resource paths");
957
962
  assert.match(server, /normalPiResourcePathsForTab[\s\S]*WEBUI_CONTROLLED_PACKAGES\.has\(packageName\)[\s\S]*continue/, "Web UI tab resource resolution should exclude separately installed Web UI feature packages");
958
963
  assert.match(server, /startedWebuiResourcePaths\(resourceType\)/, "Web UI tabs should load feature resources from the started Web UI package");
959
- assert.match(server, /workspacePackageRootForName\(nodeModulesRef\.packageName\)/, "dev Web UI should prefer top-level workspace packages for node_modules manifest entries");
964
+ assert.match(server, /resolveInstalledPackageSubpath\(nodeModulesRef\.packageName, nodeModulesRef\.subpath\)/, "Web UI should prefer workspace/global/package-root installed packages for node_modules manifest entries");
960
965
  assert.match(server, /const CODEX_TOKEN_REFRESH_SKEW_MS = 5 \* 60 \* 1000/, "server should refresh Codex OAuth tokens before they expire");
961
966
  assert.match(server, /url\.pathname === "\/api\/codex-usage" && req\.method === "GET"/, "server should expose a sanitized Codex usage endpoint");
962
967
  assert.match(server, /OPENAI_CODEX_USAGE_ENDPOINT/, "server should query Codex usage from the backend, not the browser");
@@ -1072,12 +1077,16 @@ assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should
1072
1077
  assert.match(server, /\["safetyGuard", "@firstpick\/pi-extension-safety-guard"\]/, "server should allow installing the safety guard optional feature");
1073
1078
  assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
1074
1079
  assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
1080
+ assert.match(server, /function optionalFeaturePackageStatus\(featureId\)/, "server should report optional feature package install/update status");
1075
1081
  assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
1076
1082
  assert.match(server, /PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT/, "optional feature installs should support an explicit package-manager root override");
1077
- assert.match(server, /function configuredAgentNpmRoot\(\)/, "global Web UI launches should install optional feature packages into Pi's agent npm root, not the npm global prefix");
1078
- assert.match(server, /installRootDeclaresPackage\(.*?@firstpick\/pi-package-webui/s, "optional feature installs should only reuse a node_modules parent that declares the Web UI package dependency");
1083
+ assert.match(server, /function configuredAgentNpmRoot\(\)/, "global Web UI launches should consider Pi's agent npm root for optional packages");
1084
+ assert.match(server, /installRootDeclaresPackage\(.*?@firstpick\/pi-package-webui/s, "optional feature installs should reuse a node_modules parent that declares the Web UI package dependency");
1085
+ assert.match(server, /installRootContainsPackage\(.*?@firstpick\/pi-package-webui/s, "global npm Web UI launches should also accept the prefix containing the Web UI package folder");
1086
+ assert.match(server, /resolveInstalledPackageSubpath\(nodeModulesRef\.packageName, nodeModulesRef\.subpath\)/, "started Web UI resource resolution should fall back to globally installed sibling optional packages");
1079
1087
  assert.match(server, /if \(webuiDevServer\) return installRoot/, "source-checkout Web UI launches should still use the checkout root for optional feature installs");
1080
- assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no declared package root can be found");
1088
+ assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no safe package root can be found");
1089
+ assert.match(server, /url\.pathname === "\/api\/optional-features" && req\.method === "GET"/, "server should expose optional feature package status endpoint");
1081
1090
  assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
1082
1091
  assert.match(server, /requireLocalhostRoute\(req, url\.pathname\)/, "optional feature install endpoint should use shared localhost trust policy");
1083
1092
  assert.match(server, /url\.pathname === "\/api\/skill-file" && req\.method === "GET"[\s\S]*?getSkillFileData/, "server should expose GET /api/skill-file for editable skill content");
@@ -1102,6 +1111,7 @@ assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assis
1102
1111
  assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
1103
1112
  assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
1104
1113
  assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
1114
+ assert.match(readme, /GET \/api\/optional-features/, "README should document optional feature status endpoint");
1105
1115
  assert.match(readme, /POST \/api\/optional-feature-install/, "README should document optional feature install endpoint");
1106
1116
  assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
1107
1117
  assert.match(readme, /browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications/, "README should describe blocked-tab and agent-done notifications");
@@ -1109,10 +1119,11 @@ assert.match(readme, /blocked-tab browser notifications, and optional agent-done
1109
1119
  assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
1110
1120
  assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
1111
1121
  assert.match(readme, /curates Pi resources from the Web UI package that started the server/, "README should document started-package-based Web UI feature loading");
1112
- assert.match(readme, /separately installed Web UI companion packages are ignored to avoid loading two copies/, "README should document duplicate companion suppression");
1122
+ assert.match(readme, /Companion packages installed as global\/npm-prefix siblings/, "README should document global sibling companion discovery");
1123
+ assert.match(readme, /avoiding duplicate loads while keeping global `pi-webui` launches working/, "README should document duplicate companion suppression");
1113
1124
  assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
1114
- assert.match(readme, /side panel shows each optional feature as enabled, disabled, or install-needed/, "README should document optional feature side-panel controls");
1115
- assert.match(readme, /Installing a missing feature is an explicit, warned action/, "README should document optional feature install warning behavior");
1125
+ assert.match(readme, /side panel shows each optional feature as enabled, disabled, installed-but-not-loaded, update-available, or install-needed/, "README should document optional feature side-panel controls");
1126
+ assert.match(readme, /Installing or updating a feature is an explicit, warned action with running\/failure feedback/, "README should document optional feature install and update warning behavior");
1116
1127
  assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "README should document the dev helper launcher");
1117
1128
  assert.match(readme, /sync-pi-package-symlinks\.sh[\s\S]*only one copy is loaded/, "README should document dev companion symlink setup");
1118
1129
  assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
@@ -132,6 +132,10 @@ try {
132
132
  LOCALHOST_ONLY_POST_ROUTES.has("/api/network/close"),
133
133
  "closing network access must be localhost-only like opening it",
134
134
  );
135
+ assert.ok(
136
+ LOCALHOST_ONLY_POST_ROUTES.has("/api/remote-auth/settings"),
137
+ "remote PIN auth settings must be localhost-only",
138
+ );
135
139
  } finally {
136
140
  await rm(tempDir, { recursive: true, force: true });
137
141
  await rm(outsideDir, { recursive: true, force: true });