Compare commits

...
1 Commits
Author SHA1 Message Date
clawbot df89f20099 fix: open no approval window for a site-connection prompt already answered (closes #287)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 4s
e2e / e2e-firefox (push) Failing after 5s
When a site-connection prompt was decided before the toolbar popup raised
for it had loaded, that popup was torn down, chrome.action.openPopup()
rejected, and the background opened its fallback window for the answered
approval and only then removed it. In the Chrome end-to-end suite the next
test could take that window for its own prompt and lose it under its wait.
openApprovalWindow() now returns before creating a window when the approval
is no longer pending.

The blocklist test clicked its self-closing Reject with a plain click; it
now clicks it as the other site Reject does, with the click witnessed.

Model: opus-5-5
2026-10-05 00:29:29 +00:00
5 changed files with 48 additions and 11 deletions
+7 -8
View File
@@ -619,14 +619,13 @@ The jobs **report, they do not gate.** A failure is a red mark against the
commit that a reviewer has to account for, not a hard block: whether a check commit that a reviewer has to account for, not a hard block: whether a check
blocks a merge is Gitea branch protection, which this repo does not configure. blocks a merge is Gitea branch protection, which this repo does not configure.
That is not only a statement about configuration. The Chrome suite is That is not only a statement about configuration. One report of the Chrome suite
**measurably flaky under load** — two of six runs of unmutated code on a busy **failing under load** is still open:
machine lost the approval popup out from under the dApp signing wait, always in [#290](https://git.eeqj.de/sneak/AutistMask/issues/290) records runs on a busy
the `#183` section, tracked as machine failing with `the extension opened no approval window within 30000ms`.
[#287](https://git.eeqj.de/sneak/AutistMask/issues/287). So a red `e2e-chrome` So a red `e2e-chrome` has to be read before it is believed, and that report is
has to be read before it is believed, and that flake is the blocker to ever the blocker to ever making this a required check. Do not answer it with a retry
making this a required check. Do not answer it with a retry wrapper: a suite wrapper: a suite that reruns until it is green stops being evidence.
that reruns until it is green stops being evidence.
Nothing in either job can pass vacuously. There is no `continue-on-error` and no Nothing in either job can pass vacuously. There is no `continue-on-error` and no
`|| true`; both scripts exit non-zero when docker is missing, when the image `|| true`; both scripts exit non-zero when docker is missing, when the image
+11
View File
@@ -45,6 +45,17 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-05: The extension no longer opens a window for a site-connection
prompt already answered
([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was
decided before the toolbar popup raised for it had loaded, that popup was torn
down, `chrome.action.openPopup()` rejected, and the background opened its
fallback window for the answered approval and then removed it. In the Chrome
end-to-end suite the next test could take that window for its own prompt and
lose it under its wait. `openApprovalWindow()` now opens nothing for an
approval that is no longer pending. The blocklist test's Reject, whose window
closes itself, is clicked as the other site Reject is, with the click
witnessed.
- 2026-10-04: A popup boot in the tests loads transactions without failing - 2026-10-04: A popup boot in the tests loads transactions without failing
([#429](https://git.eeqj.de/sneak/AutistMask/issues/429)). The stand-in for ([#429](https://git.eeqj.de/sneak/AutistMask/issues/429)). The stand-in for
`filterTransactions` in `tests/support/popupBoot.js` returned a bare list, `filterTransactions` in `tests/support/popupBoot.js` returned a bare list,
+5 -1
View File
@@ -413,7 +413,7 @@ function releaseApproval(approval) {
} }
} }
// Open approval in a separate popup window. // Open approval in a separate popup window, unless it is no longer pending.
// This is the primary mechanism for tx/sign approvals (triggered programmatically, // This is the primary mechanism for tx/sign approvals (triggered programmatically,
// not from a user gesture) and the fallback for site-connection approvals. // not from a user gesture) and the fallback for site-connection approvals.
// Never rejects. Its callers raise it from inside a Promise executor and drop // Never rejects. Its callers raise it from inside a Promise executor and drop
@@ -446,6 +446,10 @@ async function openApprovalWindow(id) {
); );
} }
// Already answered: a site-connection prompt decided before the toolbar
// popup raised for it had loaded, whose openPopup() rejects only now.
if (!pendingApprovals[id]) return;
let win = null; let win = null;
try { try {
win = await windowsCreate(opts); win = await windowsCreate(opts);
+21
View File
@@ -2235,6 +2235,27 @@ describe("a site connection decided as the popup closes", () => {
}); });
}); });
// The prompt is decided before the toolbar popup raised for it has
// loaded; that popup is torn down and openPopup() rejects only after.
test("a toolbar prompt already decided opens no window when openPopup() rejects", async () => {
const bg = loadBackground({ actionPopup: true });
const opening = deferred();
bg.openPopup.mockImplementation(() => opening.promise);
const pending = bg.requestSite();
await settle();
const port = bg.connectApproval(pending.id());
port.decide(true, false);
port.disconnect();
await settle();
expect(pending.result()).toEqual({ result: [signer.address] });
opening.reject(new Error("the toolbar popup closed before it loaded"));
await settle();
expect(bg.created).toHaveLength(0);
});
// The port carries a decision now, so it carries the sender check the // The port carries a decision now, so it carries the sender check the
// one-off message used to carry. A content script that guessed an // one-off message used to carry. A content script that guessed an
// approval id must not be able to connect the site it is running on. // approval id must not be able to connect the site it is running on.
+4 -2
View File
@@ -2738,7 +2738,7 @@ async function closeApprovalPages(ctx) {
// #btn-reject on the site prompt — NOT self-proving. A page that went away // #btn-reject on the site prompt — NOT self-proving. A page that went away
// without the click landing disconnects the approval port, the background // without the click landing disconnects the approval port, the background
// settles that as 4001, and 4001 is exactly what assertUserRejection // settles that as 4001, and 4001 is exactly what assertUserRejection
// accepts. That call site arms the click trace below and asserts it. // accepts. Both call sites arm the click trace below and assert it.
// //
// A button that is missing or unclickable raises a different error, which is // A button that is missing or unclickable raises a different error, which is
// rethrown. // rethrown.
@@ -3124,7 +3124,9 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
// Not remembered: a remembered decision for this origin would // Not remembered: a remembered decision for this origin would
// outlive the test. // outlive the test.
await popup.uncheck("#approve-remember"); await popup.uncheck("#approve-remember");
await popup.click("#btn-reject"); await armClickTrace(env, popup, "#btn-reject");
await clickAndClose(popup, "#btn-reject");
await assertClickLanded(env, "#btn-reject");
await assertUserRejection( await assertUserRejection(
phishingDapp, phishingDapp,