[GH-ISSUE #2520] fix(verify-real-browser): fixture startup failure leaves an empty browser profile #836

Closed
opened 2026-10-15 18:28:52 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2520

Summary

The real-browser verification command allocates a disposable browser profile before starting its local fixture server. When that server fails to listen, the command reports the startup error but leaves the empty profile directory behind. The directory belongs to this verification run and has no remaining cleanup owner.

Steps to reproduce

  1. Use the baseline under Runtime or environment with its dependencies and compiled runtime already available, plus a Chrome-family executable discoverable by the verifier. The command below runs the unchanged verifier entry script. It injects a listen error without exhausting file descriptors, records the profile allocation, and prevents any unexpected browser launch.
  2. Run this focused Node probe from the repository root. It prints the retained-profile observation and removes only the directory it recorded before exiting.
node --max-old-space-size=512 --input-type=module <<'JS'
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import cp from "node:child_process";
import { Server } from "node:http";
import { syncBuiltinESMExports } from "node:module";
const originalMkdtemp = fs.mkdtemp;
const originalSpawn = cp.spawn;
const originalListen = Server.prototype.listen;
const failure = Object.assign(new Error("controlled listen failure"), {
  code: "EMFILE",
});
let profile;
let launches = 0;
fs.mkdtemp = async (...args) => {
  profile = await originalMkdtemp(...args);
  return profile;
};
cp.spawn = () => {
  launches += 1;
  throw new Error("unexpected browser launch");
};
Server.prototype.listen = function () {
  queueMicrotask(() => this.emit("error", failure));
  return this;
};
syncBuiltinESMExports();
try {
  let thrown;
  try {
    await import("./scripts/verify-real-browser.mjs");
  } catch (error) {
    thrown = error;
  }
  assert.equal(thrown, failure);
  assert.equal(launches, 0);
  assert.ok(profile);
  assert.ok((await fs.stat(profile)).isDirectory());
  assert.deepEqual(await fs.readdir(profile), []);
  console.log(JSON.stringify({
    startup_error: thrown.code,
    chrome_launches: launches,
    profile_retained: true,
    profile_entries: 0,
  }));
} finally {
  Server.prototype.listen = originalListen;
  fs.mkdtemp = originalMkdtemp;
  cp.spawn = originalSpawn;
  syncBuiltinESMExports();
  if (profile) await fs.rm(profile, { recursive: true, force: true });
}
JS

Expected behavior

When fixture startup fails after profile allocation, the verifier should report the startup failure and remove its disposable profile. No browser should launch after that startup failure.

Actual behavior

A locally executed probe imported the unchanged entry script with a controlled server-listen error and observed that the error propagated while the empty profile directory remained. It recorded zero child-process launches. This observation proves the startup cleanup failure; it does not establish a live Chrome process leak or a later teardown failure.

{"startup_error":"EMFILE","chrome_launches":0,"profile_retained":true,"profile_entries":0}

Evidence

  • Expected source: AGENTS.md:19 assigns resource ownership and requires retaining ownership when cleanup fails; scripts/verify-real-browser.mjs:58 and :401-406 establish this entrypoint's profile allocation and deletion responsibility.
  • Failure source: scripts/verify-real-browser.mjs:59-63 awaits fixture startup before entering its cleanup-owning try; scripts/fixtures/browser-verifier-site.mjs:171-174 rejects startup on a listen error. The executed probe observed the allocated directory after that rejection.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The executed probe redirected the actual profile allocation into isolated verification scratch and instrumented child-process launches. The inline command retains the same failure injection and assertions, uses the verifier's normal temporary directory, and refuses an unexpected launch. Neither procedure edits repository source. The fault injection exercises the real fixture error handler and entrypoint control flow; it does not reproduce operating-system descriptor exhaustion or a complete real-browser run.

Runtime or environment

Failure was verified at a2d8b94dd5 and is still present at 8b181716ac by source recheck, Node v26.10.0, npm 11.19.1, macOS, with installed dependencies and existing compiled runtime. The executable selector found an accessible Chrome-family installation; Chrome was never launched.

Restoration check

Under the same controlled listen error, record the allocated profile path and inspect it after the verifier rejects. The failure is an existing empty directory; restoration is that the directory no longer exists, the startup error still reaches the caller, and no child process launches. Keep those error and launch assertions so skipping fixture startup or hiding its failure cannot count as restoration. Also confirm that ordinary verifier completion still removes its owned profile.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: The reproduced startup error leaves one empty disposable directory. No browser process, bound fixture server, user-data loss, or blocked primary workflow was established for this path.
  • Workaround status: available
  • Workaround basis: The directory is empty and owned solely by the failed verifier run; removing that recorded directory preserves the failed run's diagnostic result. The local probe performed that removal after observing the defect.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2520 ## Summary The real-browser verification command allocates a disposable browser profile before starting its local fixture server. When that server fails to listen, the command reports the startup error but leaves the empty profile directory behind. The directory belongs to this verification run and has no remaining cleanup owner. ## Steps to reproduce 1. Use the baseline under Runtime or environment with its dependencies and compiled runtime already available, plus a Chrome-family executable discoverable by the verifier. The command below runs the unchanged verifier entry script. It injects a listen error without exhausting file descriptors, records the profile allocation, and prevents any unexpected browser launch. 2. Run this focused Node probe from the repository root. It prints the retained-profile observation and removes only the directory it recorded before exiting. ```sh node --max-old-space-size=512 --input-type=module <<'JS' import assert from "node:assert/strict"; import fs from "node:fs/promises"; import cp from "node:child_process"; import { Server } from "node:http"; import { syncBuiltinESMExports } from "node:module"; const originalMkdtemp = fs.mkdtemp; const originalSpawn = cp.spawn; const originalListen = Server.prototype.listen; const failure = Object.assign(new Error("controlled listen failure"), { code: "EMFILE", }); let profile; let launches = 0; fs.mkdtemp = async (...args) => { profile = await originalMkdtemp(...args); return profile; }; cp.spawn = () => { launches += 1; throw new Error("unexpected browser launch"); }; Server.prototype.listen = function () { queueMicrotask(() => this.emit("error", failure)); return this; }; syncBuiltinESMExports(); try { let thrown; try { await import("./scripts/verify-real-browser.mjs"); } catch (error) { thrown = error; } assert.equal(thrown, failure); assert.equal(launches, 0); assert.ok(profile); assert.ok((await fs.stat(profile)).isDirectory()); assert.deepEqual(await fs.readdir(profile), []); console.log(JSON.stringify({ startup_error: thrown.code, chrome_launches: launches, profile_retained: true, profile_entries: 0, })); } finally { Server.prototype.listen = originalListen; fs.mkdtemp = originalMkdtemp; cp.spawn = originalSpawn; syncBuiltinESMExports(); if (profile) await fs.rm(profile, { recursive: true, force: true }); } JS ``` ## Expected behavior When fixture startup fails after profile allocation, the verifier should report the startup failure and remove its disposable profile. No browser should launch after that startup failure. ## Actual behavior A locally executed probe imported the unchanged entry script with a controlled server-listen error and observed that the error propagated while the empty profile directory remained. It recorded zero child-process launches. This observation proves the startup cleanup failure; it does not establish a live Chrome process leak or a later teardown failure. ```json {"startup_error":"EMFILE","chrome_launches":0,"profile_retained":true,"profile_entries":0} ``` ## Evidence - Expected source: AGENTS.md:19 assigns resource ownership and requires retaining ownership when cleanup fails; scripts/verify-real-browser.mjs:58 and :401-406 establish this entrypoint's profile allocation and deletion responsibility. - Failure source: scripts/verify-real-browser.mjs:59-63 awaits fixture startup before entering its cleanup-owning try; scripts/fixtures/browser-verifier-site.mjs:171-174 rejects startup on a listen error. The executed probe observed the allocated directory after that rejection. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The executed probe redirected the actual profile allocation into isolated verification scratch and instrumented child-process launches. The inline command retains the same failure injection and assertions, uses the verifier's normal temporary directory, and refuses an unexpected launch. Neither procedure edits repository source. The fault injection exercises the real fixture error handler and entrypoint control flow; it does not reproduce operating-system descriptor exhaustion or a complete real-browser run. ## Runtime or environment Failure was verified at a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 and is still present at 8b181716aca2cb467be499cba226abfb5ae19141 by source recheck, Node v26.10.0, npm 11.19.1, macOS, with installed dependencies and existing compiled runtime. The executable selector found an accessible Chrome-family installation; Chrome was never launched. ## Restoration check Under the same controlled listen error, record the allocated profile path and inspect it after the verifier rejects. The failure is an existing empty directory; restoration is that the directory no longer exists, the startup error still reaches the caller, and no child process launches. Keep those error and launch assertions so skipping fixture startup or hiding its failure cannot count as restoration. Also confirm that ordinary verifier completion still removes its owned profile. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: The reproduced startup error leaves one empty disposable directory. No browser process, bound fixture server, user-data loss, or blocked primary workflow was established for this path. - Workaround status: available - Workaround basis: The directory is empty and owned solely by the failed verifier run; removing that recorded directory preserves the failed run's diagnostic result. The local probe performed that removal after observing the defect.
bsakaguchi 2026-10-15 18:28:52 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Addressed on main in eca18e08ac80ac838eef78788bf82429bbb30673, fd8a2e98f.

Browser profile/site allocation occurs inside the cleanup scope. Independent fixture owners are all attempted, retaining the primary and cleanup failures; the same pattern is removed from standalone verifier paths.

Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit. Verifier trust-boundary controls cover unavailable lineage, terminal modes, idle TCP teardown, and shared fixture cleanup; no real-provider verifier success is inferred.

Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.

<!-- gh-comment-id:6159226010 --> @morluto commented on GitHub (Oct 14, 2026): Addressed on main in [eca18e08ac80ac838eef78788bf82429bbb30673](https://github.com/morluto/rea/commit/eca18e08ac80ac838eef78788bf82429bbb30673), [fd8a2e98f](https://github.com/morluto/rea/commit/fd8a2e98f). Browser profile/site allocation occurs inside the cleanup scope. Independent fixture owners are all attempted, retaining the primary and cleanup failures; the same pattern is removed from standalone verifier paths. Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit. Verifier trust-boundary controls cover unavailable lineage, terminal modes, idle TCP teardown, and shared fixture cleanup; no real-provider verifier success is inferred. Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#836
No description provided.