@fungi.computer/bonsai 0.0.1 → 0.2.0

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.
@@ -0,0 +1,104 @@
1
+ import {
2
+ repositoryBrowserFixture,
3
+ repositoryBrowserPage,
4
+ startManualBuildWorkflow,
5
+ assertNoDirectInstall,
6
+ } from "./repository-browser.fixture.mjs";
7
+ import assert from "node:assert/strict";
8
+ import console from "node:console";
9
+ import { fileURLToPath, URL } from "node:url";
10
+
11
+ // Real Settings/button and Trellis cards, controlled App-method wire replies.
12
+ // This UI law does not claim a real host, Computer or installed Release.
13
+ const output = new URL(
14
+ "../../../../.botanical/honest-errors/manual-workflow-shots/",
15
+ import.meta.url,
16
+ );
17
+ const fixture = await repositoryBrowserFixture(output);
18
+ const { browser, origin } = fixture;
19
+ try {
20
+ for (const width of [1440, 390])
21
+ for (const theme of ["light", "dark"])
22
+ for (const refused of [false, true]) {
23
+ const { page, errors } = await repositoryBrowserPage(
24
+ browser,
25
+ width,
26
+ theme,
27
+ );
28
+ await page.goto(
29
+ origin +
30
+ "test/browser/repository-settings.html?scenario=" +
31
+ (refused ? "manual-workflow-refused" : "manual-workflow"),
32
+ );
33
+ await page.getByRole("link", { name: "Settings", exact: true }).click();
34
+ await page
35
+ .getByRole("button", { name: "Make this an App", exact: true })
36
+ .click();
37
+ await startManualBuildWorkflow(
38
+ page,
39
+ "git:" + "b".repeat(40),
40
+ "edited-installation",
41
+ );
42
+ await page
43
+ .getByText("Workflow run run_settings", { exact: true })
44
+ .waitFor();
45
+ if (refused) {
46
+ await page
47
+ .getByRole("alert")
48
+ .filter({
49
+ hasText: "No hosted build backend is attached to this Computer",
50
+ })
51
+ .waitFor();
52
+ assert.equal(
53
+ await page.getByText("Release installed", { exact: true }).count(),
54
+ 0,
55
+ );
56
+ assert.equal(
57
+ await page.getByText("Release verified", { exact: true }).count(),
58
+ 0,
59
+ );
60
+ } else {
61
+ await page.getByText("Build running", { exact: true }).waitFor();
62
+ await page.getByText("Release verified", { exact: true }).waitFor();
63
+ await page.getByText("Release installed", { exact: true }).waitFor();
64
+ assert.equal(
65
+ await page.locator(".wf-step").count(),
66
+ 3,
67
+ "the retained Trellis step cards render the actual step rows",
68
+ );
69
+ }
70
+ const calls = JSON.parse(
71
+ await page.locator("#app-calls").textContent(),
72
+ );
73
+ assert.equal(
74
+ calls.filter((name) => name === "apps.startBuildWorkflow").length,
75
+ 1,
76
+ );
77
+ assert.ok(calls.includes("apps.buildWorkflowRun"));
78
+ assert.ok(
79
+ !calls.includes("apps.build"),
80
+ "the button must start the Workflow, not a separate native build",
81
+ );
82
+ assert.ok(
83
+ !calls.includes("apps.install"),
84
+ "the panel must never select a Release as an App",
85
+ );
86
+ await assertNoDirectInstall(page);
87
+ assert.deepEqual(errors, []);
88
+ await page.screenshot({
89
+ path: fileURLToPath(
90
+ new URL(
91
+ width + "-" + theme + (refused ? "-refused" : "") + ".png",
92
+ output,
93
+ ),
94
+ ),
95
+ fullPage: true,
96
+ });
97
+ await page.close();
98
+ }
99
+ console.log(
100
+ "PASS manual Build & install starts one Workflow, renders Trellis cards and preserves refusal reasons in four views",
101
+ );
102
+ } finally {
103
+ await fixture.close();
104
+ }
@@ -0,0 +1,84 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { createServer } from "vite";
4
+ import { chromium } from "playwright";
5
+ import viteConfig from "./vite.config.mjs";
6
+
7
+ /** Serve the real Settings view and its controlled App-method transport. */
8
+ export async function repositoryBrowserFixture(output) {
9
+ await mkdir(output, { recursive: true });
10
+ const server = await createServer({
11
+ ...viteConfig,
12
+ configFile: false,
13
+ server: { ...viteConfig.server, port: 0 },
14
+ });
15
+ await server.listen();
16
+ try {
17
+ const browser = await chromium.launch({ headless: true });
18
+ return {
19
+ browser,
20
+ origin: server.resolvedUrls.local[0],
21
+ async close() {
22
+ try {
23
+ await browser.close();
24
+ } finally {
25
+ await server.close();
26
+ }
27
+ },
28
+ };
29
+ } catch (cause) {
30
+ await server.close();
31
+ throw cause;
32
+ }
33
+ }
34
+
35
+ export async function repositoryBrowserPage(browser, width, theme) {
36
+ const page = await browser.newPage({
37
+ viewport: { width, height: 1000 },
38
+ colorScheme: theme,
39
+ });
40
+ page.setDefaultTimeout(10000);
41
+ const errors = [];
42
+ page.on("pageerror", (cause) => errors.push(cause.message));
43
+ return { page, errors };
44
+ }
45
+
46
+ /** Expected identity stays with each scenario; inspect the same visible fields. */
47
+ export async function assertAuthoringFields(page, expected) {
48
+ for (const [name, value] of [
49
+ ["App id", expected.appId],
50
+ ["Source revision", expected.sourceRevision],
51
+ ["Install at", expected.installationId],
52
+ ]) {
53
+ assert.equal(
54
+ await page.getByRole("textbox", { name, exact: true }).inputValue(),
55
+ value,
56
+ );
57
+ }
58
+ }
59
+
60
+ export async function startManualBuildWorkflow(
61
+ page,
62
+ sourceRevision,
63
+ installationId,
64
+ ) {
65
+ await page
66
+ .getByRole("textbox", { name: "Source revision", exact: true })
67
+ .fill(sourceRevision);
68
+ await page
69
+ .getByRole("textbox", { name: "Install at", exact: true })
70
+ .fill(installationId);
71
+ await page
72
+ .getByRole("button", { name: "Build & install", exact: true })
73
+ .click();
74
+ }
75
+
76
+ export async function assertNoDirectInstall(page) {
77
+ assert.equal(
78
+ await page
79
+ .getByRole("button", { name: "Install this Release", exact: true })
80
+ .count(),
81
+ 0,
82
+ "Only the Workflow may install; no direct install action remains",
83
+ );
84
+ }
@@ -2,10 +2,10 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
- import { createCapabilityRegistry } from "@fungi.computer/agar";
5
+ import { createCapabilityRegistry, pending } from "@fungi.computer/agar";
6
6
  import { appMethods } from "@fungi.computer/agar/backend";
7
7
  import * as z from "zod";
8
- import { repoDetail } from "../../shared/contract.js";
8
+ import { appBuildStatusInput, repoDetail } from "../../shared/contract.js";
9
9
  import { connect } from "../../web/transport.js";
10
10
  import { RouterProvider, useRouter } from "../../web/router.js";
11
11
  import { RepoPage } from "../../web/pages.js";
@@ -39,10 +39,203 @@ client.setQueryData(["events", name], { events: [] });
39
39
  client.setQueryData(["changes", name], { changes: [] });
40
40
  client.setQueryData(["agents"], { agents: [] });
41
41
  let attempts = 0;
42
+ let registrations = 0;
42
43
  let withdrawn = false;
44
+ const appCalls: string[] = [];
45
+ const build = {
46
+ jobId: "build_settings",
47
+ appId: "app_settingstest",
48
+ releaseId: "rel_settings",
49
+ sourceRevision: "git:" + "a".repeat(40),
50
+ status: "running",
51
+ detail: null,
52
+ release: null,
53
+ installation: { kind: "pending", installationId: name },
54
+ };
43
55
  const capabilities = createCapabilityRegistry();
44
56
  capabilities.provide(appMethods, {
45
57
  call: (method, inputJson) => {
58
+ if (method.startsWith("apps.")) {
59
+ appCalls.push(method);
60
+ const counter = document.getElementById("app-calls");
61
+ if (counter) counter.textContent = JSON.stringify(appCalls);
62
+ }
63
+ if (method === "apps.register") {
64
+ z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
65
+ registrations += 1;
66
+ const scenario = new URLSearchParams(location.search).get("scenario");
67
+ if (scenario === "registration-refused")
68
+ return Promise.resolve({ error: "Owner only" });
69
+ if (scenario === "registration-invalid")
70
+ return Promise.resolve({
71
+ error: "Repository main must declare a valid fungi.build recipe",
72
+ });
73
+ if (scenario === "registration-unavailable" && registrations === 1)
74
+ return Promise.resolve({ error: "App registration is unavailable" });
75
+ return Promise.resolve({
76
+ appId: "app_settingstest",
77
+ sourceRevision: "git:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
78
+ });
79
+ }
80
+ if (method === "apps.startBuildWorkflow") {
81
+ z.strictObject({
82
+ repo: z.literal(name),
83
+ sourceRevision: z.literal("git:" + "b".repeat(40)),
84
+ installationId: z.literal("edited-installation"),
85
+ requestId: z.string().min(1).max(256),
86
+ }).parse(JSON.parse(inputJson));
87
+ return Promise.resolve({
88
+ kind: "started",
89
+ runId: "run_settings",
90
+ planDigest: "sha256:" + "d".repeat(64),
91
+ });
92
+ }
93
+ if (method === "apps.buildWorkflowRun") {
94
+ z.strictObject({ runId: z.literal("run_settings") }).parse(
95
+ JSON.parse(inputJson),
96
+ );
97
+ if (
98
+ new URLSearchParams(location.search).get("scenario") ===
99
+ "manual-workflow-refused"
100
+ )
101
+ return Promise.resolve({
102
+ runId: "run_settings",
103
+ planDigest: "sha256:" + "d".repeat(64),
104
+ outcome: "terminal",
105
+ title: "Build on merge",
106
+ status: "errored",
107
+ output: null,
108
+ error: {
109
+ name: "Error",
110
+ message:
111
+ "App method refused (unavailable): No hosted build backend is attached to this Computer",
112
+ },
113
+ reviews: [],
114
+ events: [
115
+ {
116
+ nodeId: "build",
117
+ iteration: "0",
118
+ poll: 0,
119
+ state: "failed",
120
+ attempt: 1,
121
+ refusal: "dispatch_failed",
122
+ },
123
+ ],
124
+ });
125
+ const release = {
126
+ appId: "app_settingstest",
127
+ releaseId: "rel_settings",
128
+ artifact: {
129
+ artifactRef: "artifact:sha256-" + "b".repeat(64),
130
+ artifactDigest: "sha256:" + "b".repeat(64),
131
+ provenance: {
132
+ sourceRevision: "git:" + "b".repeat(40),
133
+ sourceTreeDigest: "sha256:" + "c".repeat(64),
134
+ buildRecipeDigest: "sha256:" + "d".repeat(64),
135
+ },
136
+ },
137
+ };
138
+ return Promise.resolve({
139
+ runId: "run_settings",
140
+ planDigest: "sha256:" + "d".repeat(64),
141
+ outcome: "terminal",
142
+ title: "Build on merge",
143
+ status: "complete",
144
+ output: {
145
+ installation: {
146
+ installationId: "edited-installation",
147
+ status: "enabled",
148
+ },
149
+ },
150
+ error: null,
151
+ reviews: [],
152
+ events: [
153
+ {
154
+ nodeId: "build",
155
+ iteration: "0",
156
+ state: "complete",
157
+ attempt: 0,
158
+ output: {
159
+ ...build,
160
+ sourceRevision: "git:" + "b".repeat(40),
161
+ installation: {
162
+ kind: "pending",
163
+ installationId: "edited-installation",
164
+ },
165
+ },
166
+ row: {
167
+ id: "build_settings",
168
+ title: "Build running",
169
+ detail: "git:" + "b".repeat(40),
170
+ },
171
+ },
172
+ {
173
+ nodeId: "verify",
174
+ iteration: "0",
175
+ state: "complete",
176
+ attempt: 0,
177
+ output: {
178
+ release,
179
+ summary:
180
+ "Built rel_settings of app_settingstest from git:" +
181
+ "b".repeat(40),
182
+ },
183
+ row: {
184
+ id: "rel_settings",
185
+ title: "Release verified",
186
+ detail: "Exact source verified",
187
+ },
188
+ },
189
+ {
190
+ nodeId: "install",
191
+ iteration: "0",
192
+ state: "complete",
193
+ attempt: 0,
194
+ output: {
195
+ installationId: "edited-installation",
196
+ status: "enabled",
197
+ },
198
+ row: {
199
+ id: "apps.install",
200
+ title: "Release installed",
201
+ detail: "edited-installation",
202
+ },
203
+ },
204
+ ],
205
+ });
206
+ }
207
+ if (method === "apps.buildStatus") {
208
+ const input = appBuildStatusInput.parse(JSON.parse(inputJson));
209
+ if (
210
+ new URLSearchParams(location.search).get("scenario") ===
211
+ "automatic-pending"
212
+ )
213
+ return Promise.resolve(pending(5000));
214
+ if (
215
+ "repo" in input &&
216
+ new URLSearchParams(location.search).get("scenario") !==
217
+ "automatic-needs-consent"
218
+ )
219
+ return Promise.resolve(null);
220
+ return Promise.resolve({
221
+ ...build,
222
+ status: "succeeded",
223
+ installation: { kind: "needs_consent", installationId: name },
224
+ release: {
225
+ appId: build.appId,
226
+ releaseId: build.releaseId,
227
+ artifact: {
228
+ artifactRef: "artifact:sha256-" + "b".repeat(64),
229
+ artifactDigest: "sha256:" + "b".repeat(64),
230
+ provenance: {
231
+ sourceRevision: build.sourceRevision,
232
+ sourceTreeDigest: "sha256:" + "c".repeat(64),
233
+ buildRecipeDigest: "sha256:" + "d".repeat(64),
234
+ },
235
+ },
236
+ },
237
+ });
238
+ }
46
239
  if (method === "repos.delete") {
47
240
  z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
48
241
  attempts += 1;
@@ -91,6 +284,9 @@ function Shell() {
91
284
  <h1>Repositories</h1>
92
285
  )}
93
286
  </main>
287
+ <output id="app-calls" hidden>
288
+ {JSON.stringify([])}
289
+ </output>
94
290
  <output id="attempts" hidden>
95
291
  0
96
292
  </output>
@@ -1,39 +1,93 @@
1
+ import {
2
+ repositoryBrowserFixture,
3
+ repositoryBrowserPage,
4
+ startManualBuildWorkflow,
5
+ assertNoDirectInstall,
6
+ assertAuthoringFields,
7
+ } from "./repository-browser.fixture.mjs";
1
8
  import assert from "node:assert/strict";
2
9
  import console from "node:console";
3
10
  import process from "node:process";
4
- import { mkdir } from "node:fs/promises";
5
11
  import { fileURLToPath, URL } from "node:url";
6
- import { createServer } from "vite";
7
- import { chromium } from "playwright";
8
- import viteConfig from "./vite.config.mjs";
9
12
 
10
13
  const output = new URL(
11
14
  "../../../../.botanical/repos-delete/shots/",
12
15
  import.meta.url,
13
16
  );
14
- await mkdir(output, { recursive: true });
15
- const server = await createServer({
16
- ...viteConfig,
17
- configFile: false,
18
- server: { ...viteConfig.server, port: 0 },
19
- });
20
- await server.listen();
21
- const origin = server.resolvedUrls.local[0];
22
- let browser;
17
+ const fixture = await repositoryBrowserFixture(output);
18
+ const { browser, origin } = fixture;
23
19
  const name = "disposable-merge-1791840651624";
24
20
  const before = process.argv.includes("--before");
21
+ const appWrites = async (page) =>
22
+ JSON.parse(await page.locator("#app-calls").textContent()).filter(
23
+ (method) =>
24
+ method !== "apps.buildStatus" && method !== "apps.buildWorkflowRun",
25
+ );
25
26
  try {
26
- browser = await chromium.launch({ headless: true });
27
27
  for (const width of [1440, 390])
28
28
  for (const theme of ["light", "dark"]) {
29
- const page = await browser.newPage({
30
- viewport: { width, height: 1000 },
31
- colorScheme: theme,
32
- });
33
- page.setDefaultTimeout(10000);
34
- const errors = [];
35
- page.on("pageerror", (cause) => errors.push(cause.message));
36
- await page.goto(`${origin}test/browser/repository-settings.html`);
29
+ const { page, errors } = await repositoryBrowserPage(
30
+ browser,
31
+ width,
32
+ theme,
33
+ );
34
+ if (!before) {
35
+ await page.goto(
36
+ origin +
37
+ "test/browser/repository-settings.html?scenario=automatic-needs-consent",
38
+ );
39
+ for (let visit = 0; visit < 2; visit++) {
40
+ if (visit > 0) await page.reload();
41
+ await page
42
+ .getByRole("link", { name: "Settings", exact: true })
43
+ .click();
44
+ await page
45
+ .getByRole("status")
46
+ .filter({ hasText: "Needs owner consent" })
47
+ .waitFor();
48
+ await assertAuthoringFields(page, {
49
+ appId: "app_settingstest",
50
+ sourceRevision: "git:" + "a".repeat(40),
51
+ installationId: name,
52
+ });
53
+ await assertNoDirectInstall(page);
54
+ assert.equal(
55
+ await page
56
+ .getByRole("button", { name: "Build & install", exact: true })
57
+ .isEnabled(),
58
+ true,
59
+ );
60
+ assert.deepEqual(
61
+ await appWrites(page),
62
+ [],
63
+ "Status discovery and reload must not register, build or install",
64
+ );
65
+ }
66
+ await page.screenshot({
67
+ path: fileURLToPath(
68
+ new URL(width + "-" + theme + "-automatic-reload.png", output),
69
+ ),
70
+ fullPage: true,
71
+ });
72
+ }
73
+ if (!before) {
74
+ await page.goto(
75
+ origin +
76
+ "test/browser/repository-settings.html?scenario=automatic-pending",
77
+ );
78
+ await page.getByRole("link", { name: "Settings", exact: true }).click();
79
+ await page
80
+ .getByRole("status")
81
+ .filter({
82
+ hasText: "Build or automatic installation is still running",
83
+ })
84
+ .waitFor();
85
+ assert.deepEqual(await appWrites(page), []);
86
+ }
87
+ await page.goto(
88
+ origin +
89
+ "test/browser/repository-settings.html?scenario=manual-workflow-refused",
90
+ );
37
91
  await page.evaluate(
38
92
  (value) =>
39
93
  globalThis.document.documentElement.setAttribute("data-theme", value),
@@ -56,10 +110,65 @@ try {
56
110
  await page
57
111
  .getByRole("heading", { name: "Repository settings", exact: true })
58
112
  .waitFor();
113
+ await page
114
+ .getByRole("button", { name: "Make this an App", exact: true })
115
+ .click();
116
+ await page
117
+ .locator('#app-build-app-id[value="app_settingstest"]')
118
+ .waitFor();
119
+ await assertAuthoringFields(page, {
120
+ appId: "app_settingstest",
121
+ sourceRevision: "git:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
122
+ installationId: name,
123
+ });
124
+ await page
125
+ .getByRole("button", { name: "Build & install", exact: true })
126
+ .and(page.locator(":enabled"))
127
+ .waitFor();
128
+ assert.equal(
129
+ await page
130
+ .getByRole("button", { name: "Build & install", exact: true })
131
+ .isEnabled(),
132
+ true,
133
+ );
134
+ await assertNoDirectInstall(page);
135
+ assert.deepEqual(await appWrites(page), ["apps.register"]);
59
136
  assert.equal(await page.locator("#attempts").textContent(), "0");
60
137
  await page.screenshot({
61
138
  path: fileURLToPath(new URL(`${width}-${theme}-settings.png`, output)),
62
139
  });
140
+ assert.equal(
141
+ await page
142
+ .getByRole("textbox", { name: "App id", exact: true })
143
+ .isEditable(),
144
+ false,
145
+ );
146
+ await startManualBuildWorkflow(
147
+ page,
148
+ "git:" + "b".repeat(40),
149
+ "edited-installation",
150
+ );
151
+ await page
152
+ .getByRole("alert")
153
+ .filter({
154
+ hasText: "No hosted build backend is attached to this Computer",
155
+ })
156
+ .waitFor();
157
+ await assertAuthoringFields(page, {
158
+ appId: "app_settingstest",
159
+ sourceRevision: "git:" + "b".repeat(40),
160
+ installationId: "edited-installation",
161
+ });
162
+ assert.deepEqual(await appWrites(page), [
163
+ "apps.register",
164
+ "apps.startBuildWorkflow",
165
+ ]);
166
+ await assertNoDirectInstall(page);
167
+ await page.screenshot({
168
+ path: fileURLToPath(
169
+ new URL(`${width}-${theme}-build-refusal.png`, output),
170
+ ),
171
+ });
63
172
  await page
64
173
  .getByRole("button", { name: "Delete repository", exact: true })
65
174
  .click();
@@ -88,7 +197,16 @@ try {
88
197
  hasText: "Deletion didn't finish because storage was unavailable.",
89
198
  })
90
199
  .waitFor();
91
- assert.equal(await page.getByRole("alert").locator("p").count(), 1);
200
+ assert.equal(
201
+ await page
202
+ .getByRole("alert")
203
+ .filter({
204
+ hasText: "Deletion didn't finish because storage was unavailable.",
205
+ })
206
+ .locator("p")
207
+ .count(),
208
+ 1,
209
+ );
92
210
  assert.equal(await page.locator("#attempts").textContent(), "1");
93
211
  await page.screenshot({
94
212
  path: fileURLToPath(new URL(`${width}-${theme}-retry.png`, output)),
@@ -111,6 +229,51 @@ try {
111
229
  .getByRole("alert")
112
230
  .filter({ hasText: "Only the owner" })
113
231
  .waitFor();
232
+ for (const [scenario, message] of [
233
+ ["registration-refused", "Owner only"],
234
+ [
235
+ "registration-invalid",
236
+ "Repository main must declare a valid fungi.build recipe",
237
+ ],
238
+ ["registration-unavailable", "App registration is unavailable"],
239
+ ]) {
240
+ await page.goto(
241
+ origin + "test/browser/repository-settings.html?scenario=" + scenario,
242
+ );
243
+ await page.getByRole("link", { name: "Settings", exact: true }).click();
244
+ await page
245
+ .getByRole("button", { name: "Make this an App", exact: true })
246
+ .click();
247
+ await page.getByRole("alert").filter({ hasText: message }).waitFor();
248
+
249
+ await assertAuthoringFields(page, {
250
+ appId: "",
251
+ sourceRevision: "",
252
+ installationId: name,
253
+ });
254
+ assert.equal(
255
+ await page
256
+ .getByRole("button", { name: "Build & install", exact: true })
257
+ .isDisabled(),
258
+ true,
259
+ );
260
+ await assertNoDirectInstall(page);
261
+ assert.deepEqual(await appWrites(page), ["apps.register"]);
262
+ await page.screenshot({
263
+ path: fileURLToPath(
264
+ new URL([width, theme, scenario].join("-") + ".png", output),
265
+ ),
266
+ });
267
+ if (scenario === "registration-unavailable") {
268
+ await page
269
+ .getByRole("button", { name: "Make this an App", exact: true })
270
+ .click();
271
+ await page
272
+ .locator('#app-build-app-id[value="app_settingstest"]')
273
+ .waitFor();
274
+ assert.equal(await page.getByRole("alert").count(), 0);
275
+ }
276
+ }
114
277
  assert.deepEqual(errors, []);
115
278
  assert.equal(
116
279
  await page.evaluate(
@@ -126,11 +289,10 @@ try {
126
289
  console.log(
127
290
  before
128
291
  ? "PASS baseline 1440/390 light/dark: repository view before Settings"
129
- : "PASS Settings 1440/390 light/dark: launcher entry, explicit confirmation, cancel, owner refusal, failed provider retry and home navigation",
292
+ : "PASS Settings 1440/390 light/dark: App registration/prefill, readonly identity, editable head/name and honest build refusal; typed needs-consent status without installing; denied/invalid/unavailable registration and retry; deletion confirmation/cancel/refusal/provider retry/navigation",
130
293
  );
131
294
  } finally {
132
- await browser?.close();
133
- await server.close();
295
+ await fixture.close();
134
296
  console.log("CLOSED browser and Vite server");
135
297
  }
136
298
  process.exitCode = 0;