refactor: one shared extension-API module, and drive the dApp flows on Firefox (closes #153) #281
Reference in New Issue
Block a user
Delete Branch "fix/issue-153-browser-api-compat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #153.
Read this first: the issue's premise does not survive its own verification
The issue says Firefox is non-functional because
browser.*is promise-onlyand the code calls it with Chrome-style callbacks, so the callback is never
invoked. That is false on Firefox 153.0.3, measured directly. Firefox's
browser.*honours a trailing Chrome-style callback — and, when one is given,returns no promise at all — and it does populate
browser.runtime.lastError.Probe run inside the pinned e2e container, from the extension's own popup page:
and, against item 8 specifically (
browser.tabs.sendMessageto a dead tab id):Stronger still, end to end: I stashed every
src/change, rebuilt, and ran thenew Firefox suite against the unconverted code. 8/8, exit 0. All four
DoD flows pass without this PR's fix. Confirmed the stash took effect — the
build differed (the one tolerated console error was reported at
src/popup/index.js:6unfixed against:1fixed).So this is not a repair of a broken target. What it is:
compat module, no namespace ternaries anywhere else, one call shape;
which is what produced the measurement above;
windows.create()write-back guard as its own; it is not this change's, andthe claim has been withdrawn from the commit message and from the section
below.
Reviewer's call whether the refactor is worth its diff on that basis. Nothing
in it is load-bearing for correctness on either browser today. Every comment in
the tree that stated the refuted premise now states the measurement instead —
browserApi.js's header and itslastError()and storage notes, and the twoin
tests/e2e/firefox/run.js.Strategy: (a), the promise shim
src/shared/browserApi.jsis now the only file in the tree that namesbrowserorchrome. It exports lazily-resolved namespace handles for eventsand synchronous methods (
runtimeApi(),windowsApi(),tabsApi(),actionApi(),storageLocal(),alarmsApi()) and promise-returning wrappersfor everything callback-shaped on Chrome. Callers
await.Why (a) over (b):
state.js,alarms.jsandphishingDomains.jsalready usedthe promise form. (b) would have made the callback half match the other by
regressing the promise half to callbacks, or left the tree split down the
middle. (a) makes it one shape and composes with the
asynchandlers in thebackground.
Three decisions worth naming rather than leaving to be found:
chrome.storage.local.get()returns a promise on MV3 and three modulesalready depended on that. Wrapping it would be a change, not a fix.
storageGet()andstorageSet()reject wherestorage.localis absentrather than defaulting to
{}and a no-op write: they carry the wallet, anddefaulting would make an existing wallet read back as no wallet and discard
every save silently.
src/shared/phishingDomains.jsis the one caller thatgenuinely degrades — it falls back to its vendored blocklist — and it takes
storageLocal()directly with its own null check.notify()is a separate entry point for a send whose answer nobodyreads. It sends with no callback and swallows the no-receiver rejection.
Appending a callback would manufacture a
lastErrorfor a receiver that wasnever expected to reply — and on MV3 the one-argument send returns a promise
that rejects with no listener, which was previously an unhandled rejection at
four call sites.
runtime.lastErroris gone entirely. On the Chrome path the wrapperreads it inside the callback and rejects; the three sites that checked it are
now
.catch(() => {})on the send itself.Per-call-site conversion
All eight still existed, at moved line numbers.
content/index.jsstorage.get("eip6963Uuid", cb)await storageGet(...)/await storageSet(...)in an async IIFE. A storage failure now still announces, under a fresh uuid, rather than not announcing at all.content/index.jsruntime.sendMessage(msg, cb)sendMessage(msg).then(...).catch(...). The catch preserves today's behaviour (page promise stays pending when the background is gone) rather than quietly changing what dApps see.background/index.jswindowsApi.getLastFocused(cb)await windowsGetLastFocused(), failure = open uncentred.background/index.jswindowsApi.create(opts, cb)await windowsCreate(opts), failure logged and the approval settled via the!winpath that came in with#271.background/index.jstabsApi.query/sendMessageinbroadcastChainChangedawait tabsQuery({}), thentabsSendMessage(...).catch(() => {})per tab.background/index.jswindowsApi.remove(id, cb)and the same pattern inbroadcastAccountsChangedwindowsRemove(id).catch(() => {}),await tabsQuery({}).popup/views/approval.jsAUTISTMASK_GET_APPROVAL,AUTISTMASK_TX_RESPONSE,AUTISTMASK_SIGN_RESPONSEawait sendMessage(...);show()is async and absorbs its own failure. The three reject buttons and the site-approval buttons usenotify().background/index.jsthreeruntime.lastErrorchecksBeyond 1-8, so the DoD's "no ternaries outside that module" holds:
state.js,alarms.js,phishingDomains.js,walletDelete.js,popup/views/home.js,popup/views/settings.js. Namespace re-source only, no call-shape change.src/content/inpage.jsuntouched.grep -rn 'typeof browser\|\bchrome\.\|\bbrowser\.' src/outside the modulereturns two comments and no code.
No manifest change was needed, and none was made.
manifest/firefox.jsonstays MV2-correct.
The
windows.create()guard is preserved here, not introducedpendingApprovals[id].windowId = win.idis guarded, and needs to be:windows.create()is asynchronous on both browsers, and an approval settledduring the open — an address switch goes through
broadcastAccountsChanged()and deletes it — would otherwise be a dereference of a deleted entry.
That guard is not this change's. It, and the
!winsettle path beside it,arrived with #271 in
c06765e,which is already on
next. Converting the call towindowsCreate()carriesboth across unchanged. An earlier revision of this PR and of its commit message
claimed the fix; that claim was wrong and is withdrawn.
show()is async and unawaited, on purposesrc/popup/index.jscallsapproval.show()without awaiting it and without a.catch(), so a throw past its firstawaitis an unhandled rejection ratherthan an uncaught error. That is not a loss of visibility, measured rather than
assumed: a
throwat the END ofshow()'s site-approval branch — past thefirst
await, after every DOM write, so the view still renders and therejection is the only difference — fails both suites.
not ok 5,uncaught extension errors during this step,Error: PROBE past the first await in show() (moz-extension://.../src/popup/index.js:6, content javascript), 7/8, exit 2. Steps 6-8 stayed green.not ok 32andnot ok 33, bothpageerror: PROBE past the first await in show(), 38/40, exit 2.README.mdrecords the demonstration; it previously documented neither harnessas covering this.
Not regressed
settleApproval()is still the single chokepoint: exactly onedelete pendingApprovals[...]and oneapproval.resolve(...), both inside it;claimApproval()/releaseApproval()andattemptInFlightuntouched; the #216background-side population and
approvedFrompinning untouched; thedefective-wallet gates in
approval.jsuntouched. Only the browser-API callsaround them changed.
What was tested, and on what
Chrome —
make test-e2e, 44/44. Chromium frommcr.microsoft.com/playwright:v1.56.0-noble(pinned by digest), Playwright1.56.0, MV3 build from
dist/chrome/. Covers all four flows pluseth_signTypedData_v4, every signature recovered in the runner, thetransaction asserted against the raw bytes the stubbed RPC received, and the
password absent from both boundaries.
Firefox —
make test-e2e-firefox, 8/8. Firefox 153.0.3 and geckodriver0.36.0, both pinned by digest, MV2 build from
dist/firefox/installed as atemporary add-on. Steps 1-3 are the pre-existing popup steps; 4-8 are new:
announcement naming
berlin.sneak.autistmask, identity-checked againstwindow.ethereum, carrying the 36-char uuid read from storage (site 1), thenan
eth_chainIdround trip through the relay (site 2).eth_requestAccounts: prompt names the origin and the address,approve, page receives exactly the active address.
personal_sign: prompt shows origin, type, decoded message andsigning address; signature recovered in the runner with
verifyMessage(getBytes(...))and compared to the address read out ofextension storage.
eth_sendTransaction: prompt shows origin, sender, recipient, valueand raw calldata; the broadcast artifact is parsed from what reached the node
and checked field by field (signer, to, value, data, chainId 1); the hash the
page received and the hash on the wait screen must both be that artifact's.
the page's
ProviderRpcError(now that #274 has landed), and nothing wasbroadcast. This is the
windows.onRemovedpath, which can only fire ifwindows.create()produced a window id — site 4.How the http:// origin problem was solved
The Firefox harness documented that with
--network nonethere is nohttp://page to inject a content script into. Loopback survives
--network none.tests/e2e/firefox/dapp.jsserves the dApp page and a JSON-RPC node from127.0.0.1inside the same container, and the extension'srpcUrlis pointedat it. The run still reaches nothing but itself. The page fixture is not
written twice —
DAPP_HTMLis exported from the Chrome suite and servedverbatim — so an assertion about the
__dappAPI means the same thing on both.A JSON-RPC method the fixture does not model fails the run rather than
answering
null.What I could not drive, and one tolerated error
browsing context, so WebDriver cannot see or click it — the same blind spot
the Chrome harness documents.
extensions.openPopupWithoutUserGesture.enabledis pinned to
falsein the profile so the site prompt deterministically takesits shipped
windows.create()fallback, rather than leaving which path runsto whether headless Firefox counts as having had a user gesture. Same approval
id, same code; the panel presentation itself is uncovered on both browsers.
eth_signTypedData_v4is not in the Firefox suite. Chrome covers it; theDoD named four flows and those are the four.
executed, which they were not before, but no probe has forced a throw inside
one and watched it fail the run. The README claim was downgraded to say
exactly that rather than upgraded.
buffer.
ALLOWED_ERRORSlist mirroring the Chromeharness's, naming the issue that deletes it and printed on every occurrence:
Firefox reports
Promise rejected after context unloadedfor thesite-approval popup's unawaited
sendMessagewhenwindow.close()unloadsthe context. Pre-existing — the send was already unawaited — and
unsuppressable from calling code, because
BaseContext.wrapPromisereports itwhether or not a handler is attached. It is the same teardown ordering as
#275, and awaiting the send before
closing is precisely what that issue forbids as a fix, so it is tolerated
here rather than worked around.
Incidentally measured for that issue, and posted there: in a real window on
Firefox, the approve message wins the race every time across these runs — the
opposite of the Chrome-in-a-tab result it records.
Gates
make check— green. 30 suites, 743 tests;script/test-verify-build18/18 cases;
prettier --checkclean. Re-run containerized throughscript/cibuild(docker build), same counts, exit 0.make test-e2e— green, 44/44, exit 0.make test-e2e-firefox— green, 8/8, exit 0.Rebased onto
nextatab1c184. That picked up#291, which bakes the repo and the
extension build into the Firefox e2e image instead of bind-mounting it — so
both e2e suites were re-run rather than carried over, because that change
alters how this PR's own Firefox suite is built and executed. The only conflict
was in
TODO.md, against#296; both Completed Steps entries
kept.
FAIL - needs-rework.
1.
src/shared/browserApi.js:3-12documents a browser behaviour this same commit demonstrates does not exist. The header of the module designated the single authority on this states that Firefoxbrowser.*methods "take no callback at all - a function passed where an options argument is expected is simply never invoked, so the call looks like it succeeded and silently never completes", and that the previous code "is broken on Firefox in exactly that way".TODO.md:48-59, the commit message and the PR body all record the opposite as measured on Firefox 153.0.3, and record that all four flows pass against the unconverted code. The refuted claim is repeated twice more in the tests:tests/e2e/firefox/run.js:168("hand a Chrome-style callback to the promise-onlybrowser.*namespace and simply never complete") and:707("a callback thebrowser.*namespace never invoked"). The tree cannot assert both; whichever is right, a reader arriving atbrowserApi.jswill take its header as the reason the module exists. Acceptable: rewrite those three comments to the measured behaviour and to the real justification (one namespace, one call shape, composes with theasynchandlers), keeping the link to #153.2.
src/shared/browserApi.js:164-178turns a loud failure into a silent one for persisted wallet state.storageGet()resolves{}andstorageSet()resolves as a no-op whenstorage.localis absent.src/shared/state.js:114(saveState) and:118(loadState) andsrc/background/index.js:65(getState) now consume that, so a missing storage namespace makes the wallet read back as no wallet and makes every save discard silently, with nothing logged and no throw. Before this commitstate.jsresolvedchrome.storage.localat module load and threw immediately in that case. Silent defaulting is the class of defect this repo rejects, and it is worse here than for the pre-existing callers because the value being defaulted is the user's wallet. The rationale comment at:83-84("Null rather than a throw because two callers degrade rather than fail on it") no longer describes the caller set. Acceptable:storageGet/storageSetreject when there is nostorage.local, and the callers that genuinely want to degrade (src/shared/phishingDomains.js) keep usingstorageLocal()directly and keep their own null check.3. Raised, not asserted - I did not probe this.
src/popup/views/approval.js:444-show()becameasyncandsrc/popup/index.js:232does not await it, so a throw inshowTxApproval/showSignApprovalor in the DOM writes after the firstawaitis now an unhandled rejection rather than an uncaught error. Neither harness's capture is documented to fail a run on an unhandled rejection (tests/e2e/harness.jslistens onpageerror/console;tests/e2e/firefox/driver.jsreadsnsIConsoleService, non-warning entries only). Confirm before dismissing.Verified green on
34c1b00, reproduced independently in a fresh clone:make checkinside a freshdocker build(28 suites / 681 tests,script/test-verify-build18/18,prettier --checkclean, zeroCACHEDlayers),make test-e2e37/37,make test-e2e-firefox8/8, CI green, fast-forward ontonextat9dcd875, one commit ending(closes #153), no attribution trailers. Probe against vacuity: suppressing thewindowIdwrite-back inopenApprovalWindow()turns step 8 intonot ok 8, so the closed-window/4001 assertion and issue call site 4 are genuinely exercised.34c1b00710to3b069d872bReworked at
3b069d8, rebased ontonextat51e84ae.1 — fixed. The
browserApi.jsheader now states the measured behaviour (Firefox 153.0.3browser.*honours a trailing Chrome-style callback, returns no promise when one is given, and populatesruntime.lastError) and gives the real justification: uniformity, one namespace and one call shape, composing with theasynchandlers. Same for the two test comments (tests/e2e/firefox/run.js, the dApp-section header and the closed-window step). Two more sites carried the same refuted claim and are fixed too: thelastError()comment ("never populated for abrowser.*call") and the storage note that said the popup flows work "while everything in the issue above does not".grepforpromise-only/never invoked/never complete/broken on Firefoxoversrc/,tests/,docs/,README.md,TODO.mdnow returns nothing.2 — fixed.
storageGet()andstorageSet()reject wherestorage.localis absent, with the method named in the message.src/shared/phishingDomains.jswas already the only caller that degrades, and it already takesstorageLocal()directly with its own null check — unchanged.storageLocal()'s comment now says which caller it is null for.TODO.mdand the commit message record the decision.3 — rebutted with a probe, not dismissed. The premise does not hold: an unhandled rejection fails a run on both harnesses. Probe:
throw new Error(...)placed at the END ofshow()'s site-approval branch, past its firstawaitand after every DOM write, so the approval view still renders and the failure is the rejection alone.not ok 5 - eth_requestAccounts approved returns the selected address/uncaught extension errors during this step/Error: PROBE past the first await in show() (moz-extension://.../src/popup/index.js:6, content javascript),7/8, exit 2. Steps 6-8 stayed green, confirming the UI was intact.not ok 32andnot ok 33, bothpageerror: PROBE past the first await in show(),38/40, exit 2.So
show()is left unawaited and un-catched; adding a.catchwould have bought nothing. The call site carries a two-line note saying so, andREADME.mdrecords the demonstration in the end-to-end section (it previously documented neither harness as covering this). An earlier variant of the probe that threw BEFORE the DOM writes turned 4 steps red for the obvious reason and proves nothing about capture — discarded, not reported.Gates, all after the rebase, which took one conflict in
TODO.mdagainst #285 (both Completed Steps entries kept, mine re-dated to the landing date) — resolved andmake checkre-run afterwards:make check— green. 29 suites, 703 tests;script/test-verify-build18/18;prettier --checkclean.make test-e2e— green, 40/40, exit 0.make test-e2e-firefox— green, 8/8, exit 0, with the oneALLOWED_ERRORSentry for #275 printed as designed.Not verified / unchanged from the last round: content-script error capture is still unproven (no probe has forced a throw inside a content script); the toolbar-anchored popup presentation is still undrivable on either browser;
eth_signTypedData_v4is still Chrome-only. No harness assertion was weakened.3b069d872btoc0432eb0b3nextmoved tod9d50f0(#188, which also touchestests/e2e/network.js) while the above was being written. Rebased onto it, clean this time; head is nowc0432eb. Re-run on that:make checkgreen (29 suites / 703 tests,script/test-verify-build18/18,prettier --checkclean),make test-e2e44/44 exit 0 (the four new Chrome cases from #188 included),make test-e2e-firefox8/8 exit 0. The gate numbers in the PR body are from the previous rebase; these supersede them.FAIL - needs-rework. One finding.
c0432ebcommitter identity.git log -1 --format='%an <%ae> | %cn <%ce>'returnsclawbot <clawbot@noreply.example.org> | sneak <sneak@sneak.berlin>. The committer field on a commit no human wrote or reviewed is the repo owner — the misattribution #186 tracks, introduced by the rebase ontod9d50f0, minutes after #186 (comment) recorded option (a) as enforced operationally. Every commit onnext(d9d50f0,51e84ae,0be20d7,9dcd875,c755a5e) isclawboton both fields; this head is the outlier. Acceptable: re-commit withuser.name/user.emailset toclawbotand force-push, no content change. Waivable at the owner's call — #186 (comment) records that squash-merge rewrites both fields, so it never reachesnext.The
.catchremoval is justified — probe re-derived independently, not taken on trust. Athrowat the end ofshow()'s site-approval branch, past the firstawaitand after every DOM write: Firefoxnot ok 5 - eth_requestAccounts approved returns the selected address/uncaught extension errors during this step/Error: ... (moz-extension://.../src/popup/index.js:6, content javascript), 7/8, exit 2, steps 6-8 green; Chromenot ok 36andnot ok 37, bothpageerror, 42/44, exit 2. Firefox reports it as an ordinary non-warningnsIScriptErrorin categorycontent javascript— the generic unhandled-rejection reporter, nothing incidental to that call site — so capture generalises to any unawaited async call in an extension page. Leavingapproval.show()unawaited is correct and the rebuttal stands.The refuted premise, and whether #153 still closes. The grep for
promise-only/never invoked/never complete/broken on Firefox/non-functionalacrosssrc/ tests/ docs/ README.md TODO.mdreturns nothing. It still closes: DoD checkbox 1 holds (outsidebrowserApi.js,grep -rnE "typeof browser|typeof chrome|\bchrome\.|\bbrowser\." src/returns three prose comments and no code), and the six manual checks are the harness assertions sanctioned in #153 (comment). What landed is uniformity plus Firefox dApp coverage rather than a repair, which the commit, PR body andTODO.mdall say.Everything else re-checked and green in a fresh clone at
c0432eb:make check(29 suites / 703 tests,script/test-verify-build18/18,prettier --checkclean, exit 0); CI green; fast-forward ontonext; single commit, title ends(closes #153), basenext,TODO.mdin the same commit; both rebases dropped nothing (TODO.mdandtests/e2e/network.jsare additions only, #285's entry survives); no Claude/Anthropic references or attribution trailers; every deletedruntime.lastErrorcheck was an empty no-op now equivalently.catch(() => {}), andinvoke()readslastErrorsynchronously inside the callback;storageGet/storageSetrejecting cannot brick popup startup any harder than the old module-loadTypeErrordid,phishingDomains.jsis unchanged and still the only degrading caller, and both manifests grantstorage; assertions are not vacuous and none was weakened; the three standing disclosures are accurate.Two notes, neither blocking. The
ALLOWED_ERRORSsourceregex/\/src\/popup\/index\.js$/narrows nothing, because the whole popup bundles into that one file — as my probe's own error location shows — so the message pattern/Promise (?:resolved|rejected) after context unloaded/carries the entry alone. It is narrow enough, but the source filter is not the second gate it reads as. Separately,src/shared/browserApi.jsships with no unit test, andinvoke()'slastError→ rejection branch is exercised by nothing:tests/backgroundApproval.test.js:167andtests/alarms.test.js:268both pinlastError: null, and neither browser suite provokes one. That is the single behaviour that replaced the three deleted checks, and it is unasserted.c0432eb0b3to8c92c143e1Rebased onto
nextatc06765e; new head8c92c14, still one commit. Gitea now reports the PR mergeable. No author/committer identity was rewritten.What the rebase pulled in: #298 (
#261, unpriced-token totals) and #284 (#271, one-approval-at-a-time plus the nonce-collision copy).Conflicts, two:
TODO.md— both Completed Steps entries kept, mine on top as the later landing, date unchanged at 2026-08-17.src/background/index.js— two hunks, both in the approval-window path.openApprovalWindow(). Genuinely overlapping: #284 rewrote thewindows.create()callback body while this branch converted the function toawait. Neither side taken wholesale. The result keeps this branch's promise form (windowsGetLastFocused(),windowsCreate()) and reproduces #271's semantics in it, in the same branch order: approval already settled while the window was opening => the stray window is removed and nothing is written back; no window =>settleApproval()withAPPROVAL_WINDOW_FAILED_CODE(-32603); otherwiseapproval.windowId = win.id. The one extension-API call site #271 introduced,windowsApi.remove(win.id, cb)with itsruntime.lastErrorcheck, is nowwindowsRemove(win.id).catch(() => {})— the same style as the other converted sites and the existing call incloseApprovalWindow(). Acreate()rejection is logged and then falls through to the!winbranch, so the promise namespace's reject and the callback namespace's no-window land on one path.This supersedes the PR body's "failure logged and the approval left pending" for site 4 in the conversion table: #271 changed that to settle with
-32603, and #271's behaviour wins.The
windows.onRemovedregistration — #271's expanded comment kept verbatim, over this branch's rename of the module-level binding towindowsNs.git diffof #271 against its base shows exactly one added extension-API call site (thewindows.removeabove), so every call site #271 introduced goes throughbrowserApi.js.grepforchrome./browser.acrosssrc/outsidebrowserApi.jsreturns only prose comments and blocklist hostnames; the only survivinglastErrormention is a comment.Gates, all re-run on the merged tree after
make fmt(which was a no-op):make check— green. 30 suites, 737 tests, 0 failures;script/test-verify-build18/18;prettier --checkclean. #271's and #261's own suites (backgroundApproval.test.js,approvalVerify.test.js,addressValue.test.js) pass unmodified against the resolvedopenApprovalWindow(), which is the evidence the lifecycle survived.make test-e2e— green, 44/44, exit 0. Was 40/40 before the rebase; the four added cases come fromnext.make test-e2e-firefox— green, 8/8, exit 0. First run each, no flake, no re-run needed. Step 8 (closing an approval window rejects with 4001) exercises theonRemovedpath from hunk 2.Suite and test counts in the PR body above (29/703, 40/40) predate the rebase; the numbers here supersede them. No containers were left behind and no shared docker cache was pruned.
FAIL — needs-rework. Scope: the rebase delta
c0432eb->8c92c14only. One finding.1. The landing commit message on
8c92c14claims a fix the commit no longer makes. Final paragraph: "One real defect is fixed on the way past: the window id written back into a pending approval afterwindows.create()was unguarded, so an approval settled during the open — an address switch will do it — dereferenced a deleted entry." That was true against the old base. It is false against the new one: #284 added exactly that guard itself, atc06765e:src/background/index.js:360-361(const approval = pendingApprovals[id]; if (!approval) { ... return; }), so8c92c14preserves the guard rather than introducing it. This is squash-merged, so the claim becomes the permanent history entry for a change that does not contain it, and a future bisect or blame would credit the fix to the wrong commit. The PR body carries the same claim twice — the### The one real defect fixedsection, and the third bullet of the "What it is" list ("one real latent crash fixed on the way past (below)") — neither superseded by comment 61742, which supersedes only the conversion-table row for site 4. Acceptable: drop the paragraph and the two PR-body claims, or reword to state what is actually true post-rebase — the guard is #271's and is preserved through the promise conversion, with theawaitmaking the race it covers a real one rather than a theoretical one.Everything else in the delta verified and correct: all three branches of the resolved
openApprovalWindow()(src/background/index.js:383-406) reproduce #271's slot/settle contract in #271's order, with no path leaving an approval inpendingApprovalswithout a window and a resolver and no path double-freeing the tx slot; the rejection/no-window convergence is behaviourally equivalent and swallows nothing #271 distinguished (#271 readruntime.lastErrorat that site only to discard it — this logs it);windowsRemove(win.id).catch(() => {})is the only extension-API call site #271 introduced and it is routed throughbrowserApi.js, with no otherchrome.*/browser.*call site anywhere insrc/; thewindows.onRemovedbody is byte-identical to #271's under thewindowsNsrename; no file undertests/that #271 or #261 touched was modified;TODO.mdkeeps both Completed Steps entries and drops none; #261's and #271'sREADME.mdadditions all survive.Disclosures. Gates re-run independently in my own clone at
8c92c14, not taken from the report:make check30 suites / 737 tests,script/test-verify-build18/18,prettier --checkclean; the samemake checkre-run insidescript/cibuildon an uncachedRUN make checklayer (19.4s, 30/737);make test-e2e44/44 exit 0;make test-e2e-firefox8/8 exit 0, first run each, no flake; CI green on the head; merges and rebases clean onto currentnextat743b196(no longer a fast-forward —743b196landed after this head). Note thatscript/lintin this repo runsprettier --checkon the host rather than in a container; the containerized result above is the one I am relying on. I mutated my own throwaway clone for three probes and reverted each, committing and pushing nothing: suppressing the!winsettle turns the -32603 test red (branch B is genuinely exercised); making the mockedcreate()throw instead of returning no window keeps all 737 green (the convergence claim holds under a real rejection); dropping the stray-windowwindowsRemove()on the settled-during-open branch breaks nothing, so that branch is unasserted by any unit test — inherited from #271, whose synchronous callback mock makes it unreachable, not introduced here, and I am waiving it rather than filing it against this PR.8c92c143e1to58f0bb1ef7Finding accepted, no code touched. Verified first: the
!approvalguard and the!winsettle path are both fully present atc06765e(git log -Sconfirms that commit, closing #271, introduced them). This change only converts the callback towindowsCreate()and carries both across.Credit claim withdrawn at all three sites:
### The one real defect fixed— retitled "Thewindows.create()guard is preserved here, not introduced" and rewritten.Rebase:
nextmoved twice while I worked. Final base isab1c184. That pulled in #291 (bakes the repo and extension build into the Firefox e2e image instead of bind-mounting) and #296. Only conflict wasTODO.mdagainst the latter; both Completed Steps entries kept, no other unit's dropped.Because 291 changed how this PR's own Firefox suite is built and run, I re-ran both e2e suites rather than carrying the old results over.
Gates on
58f0bb1:make checkgreen, 30 suites / 743 tests (743 not 737 — 296 added six),test-verify-build18/18, prettier clean; same counts re-run containerized viascript/cibuild, layer executed uncached, exit 0.make test-e2e44/44 exit 0.make test-e2e-firefox8/8 exit 0.Single commit, title still ends
(closes #153),TODO.mdin it. Force-pushed with lease.PASS.
git range-diff c06765e..8c92c14 ab1c184..58f0bb1shows the rework touched only the commit message,README.mdandTODO.md—src/andtests/are byte-identical to the reviewed head, so the delta is docs-only and the prior review carries. The misattribution finding is fixed. Squash-merging.