Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28f8a43f2f | ||
|
|
6a86b726d2 | ||
|
|
18bdafd130 |
@@ -22,11 +22,10 @@ on: [push]
|
||||
# These jobs REPORT, they do not gate. Whether a check blocks a merge is
|
||||
# Gitea branch protection, which this repo does not configure, so a failure
|
||||
# here is a red mark a reviewer has to account for rather than a hard
|
||||
# block. Making e2e-chrome a required check is blocked on the measured
|
||||
# flake in the dApp signing wait -- two of six runs of unmutated code on a
|
||||
# loaded machine -- tracked as
|
||||
# https://git.eeqj.de/sneak/AutistMask/issues/287. A gate that fails at
|
||||
# random teaches people to merge past red.
|
||||
# block. Making e2e-chrome a required check is blocked while reports of the
|
||||
# Chrome suite failing under load are still open; the "In CI" section of
|
||||
# README.md names them. A gate that fails at random teaches people to merge
|
||||
# past red.
|
||||
#
|
||||
# Nothing here may pass vacuously. There is no continue-on-error and no
|
||||
# `|| true`. Both scripts exit non-zero when docker is missing, when the
|
||||
|
||||
@@ -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
|
||||
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
|
||||
**measurably flaky under load** — two of six runs of unmutated code on a busy
|
||||
machine lost the approval popup out from under the dApp signing wait, always in
|
||||
the `#183` section, tracked as
|
||||
[#287](https://git.eeqj.de/sneak/AutistMask/issues/287). So a red `e2e-chrome`
|
||||
has to be read before it is believed, and that flake is the blocker to ever
|
||||
making this a required check. Do not answer it with a retry wrapper: a suite
|
||||
that reruns until it is green stops being evidence.
|
||||
That is not only a statement about configuration. A report of the Chrome suite
|
||||
**failing under load** is still open:
|
||||
[#290](https://git.eeqj.de/sneak/AutistMask/issues/290), runs on a busy machine
|
||||
failing with `the extension opened no approval window within 30000ms`. So a red
|
||||
`e2e-chrome` has to be read before it is believed, and those failures are the
|
||||
blocker to ever making this a required check. Do not answer them with a retry
|
||||
wrapper: a suite that reruns until it is green stops being evidence.
|
||||
|
||||
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
|
||||
|
||||
@@ -51,9 +51,36 @@ but the review is broader than any of them.
|
||||
close before the save lands loses the switch; with the network left on
|
||||
Sepolia, a dozen later tests failed too. Each Settings switch and spam-filter
|
||||
toggle is now waited for in storage before the close, and `reopenPopup()`
|
||||
waits until the view it expects to reopen on is the saved one. A change made
|
||||
while an earlier save from the same page is still running is lost even without
|
||||
a close; that is [#448](https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||
waits until the view it expects to reopen on is the saved one. The restore
|
||||
half of the round trip and the second filter toggle change a setting right
|
||||
after a reopen, while the reopened popup's own saves may still be running;
|
||||
they rely on the fix for
|
||||
[#448](https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||
|
||||
- 2026-10-05: A change made while an earlier save from the same page is still
|
||||
running is stored ([#448](https://git.eeqj.de/sneak/AutistMask/issues/448)).
|
||||
`saveStateOnce()` took its baseline from the page's state after the write, so
|
||||
a change made while the save waited on storage counted as already stored and
|
||||
the save queued after it wrote nothing. A setting changed during the read was
|
||||
lost; so was a wallet added, a site revoked or an endpoint changed during the
|
||||
write, and a wallet deleted then stayed in storage. The save now copies the
|
||||
page's fields when it starts, writes from that copy, and keeps the copy as the
|
||||
baseline.
|
||||
|
||||
- 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. Making `e2e-chrome` a required check is still blocked: other
|
||||
reports of the Chrome suite failing under load are open, among them
|
||||
[#290](https://git.eeqj.de/sneak/AutistMask/issues/290) and
|
||||
[#446](https://git.eeqj.de/sneak/AutistMask/issues/446), as `README.md` says.
|
||||
|
||||
- 2026-10-05: The lost-password delete confirmation refuses an empty field and
|
||||
ignores characters that paint nothing
|
||||
|
||||
@@ -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,
|
||||
// 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
|
||||
@@ -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;
|
||||
try {
|
||||
win = await windowsCreate(opts);
|
||||
|
||||
+12
-5
@@ -122,9 +122,9 @@ function currentNetwork() {
|
||||
return networkById(state.networkId);
|
||||
}
|
||||
|
||||
// The persisted fields as they stood at the end of this page's last
|
||||
// loadState() or saveState(). saveState() diffs the live state against this
|
||||
// to find only the fields THIS page actually changed.
|
||||
// The persisted fields as this page held them when its last loadState()
|
||||
// finished, or when its last successful saveState() began. saveState() diffs
|
||||
// the live state against this to find only the fields THIS page changed since.
|
||||
//
|
||||
// Deep-cloned, not a reference: callers mutate persisted objects and arrays
|
||||
// in place (state.wallets.push(...)), and a reference baseline would mutate
|
||||
@@ -464,7 +464,10 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
// does not own goes on being whatever its last loadState() saw, same as
|
||||
// before this fix; only the persisted record is guaranteed current.
|
||||
async function saveStateOnce() {
|
||||
const current = snapshotPersisted();
|
||||
// A copy, so what this save compares and writes is the page's state as it
|
||||
// stood when the save began. A change made while it waits on storage is
|
||||
// left for the next save, which compares against this copy.
|
||||
const current = structuredClone(snapshotPersisted());
|
||||
const result = await storageGet("autistmask");
|
||||
// The record in storage right now is about to be merged into and written
|
||||
// back, so it is validated exactly like a load validates it. Without this,
|
||||
@@ -521,7 +524,11 @@ async function saveStateOnce() {
|
||||
// exactly as it stood; see the note above.
|
||||
rawState.hasWallet = rawState.wallets.length > 0;
|
||||
|
||||
baseline = structuredClone(snapshotPersisted());
|
||||
// What this save compared and wrote, not the page's state now: a change
|
||||
// made during the save must still differ from the baseline, or the save
|
||||
// queued after it finds nothing to store
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||
baseline = current;
|
||||
}
|
||||
|
||||
// showView() calls saveState() on every navigation without awaiting it, so
|
||||
|
||||
@@ -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
|
||||
// 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.
|
||||
|
||||
+4
-2
@@ -2812,7 +2812,7 @@ async function closeApprovalPages(ctx) {
|
||||
// #btn-reject on the site prompt — NOT self-proving. A page that went away
|
||||
// without the click landing disconnects the approval port, the background
|
||||
// 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
|
||||
// rethrown.
|
||||
@@ -3198,7 +3198,9 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
|
||||
// Not remembered: a remembered decision for this origin would
|
||||
// outlive the test.
|
||||
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(
|
||||
phishingDapp,
|
||||
|
||||
@@ -423,3 +423,80 @@ describe("two wallets independently created with a colliding identity", () => {
|
||||
expect(secrets).toContain("secret-b");
|
||||
});
|
||||
});
|
||||
|
||||
// showView() saves on every navigation without waiting, so the user can change
|
||||
// something while that save is still waiting on storage. The change is followed
|
||||
// by its own saveState(), which runs after the first save; it must be stored
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||
describe("a change made while an earlier save from the same page is running", () => {
|
||||
// Runs `change` inside the next call to `op` (the stub's get or set),
|
||||
// before that call does its work.
|
||||
function runInside(op, change) {
|
||||
const real = op.getMockImplementation();
|
||||
op.mockImplementationOnce(async (arg) => {
|
||||
change();
|
||||
return real(arg);
|
||||
});
|
||||
}
|
||||
|
||||
test("a network switched during the earlier save's read is stored", async () => {
|
||||
const storage = makeStorageStub({
|
||||
autistmask: { wallets: [W1], networkId: "sepolia" },
|
||||
});
|
||||
const { state, saveState, loadState } = loadPage(storage).state;
|
||||
await loadState();
|
||||
|
||||
let queued;
|
||||
runInside(storage.get, () => {
|
||||
state.networkId = "mainnet";
|
||||
queued = saveState();
|
||||
});
|
||||
state.theme = "dark";
|
||||
await saveState();
|
||||
await queued;
|
||||
|
||||
const stored = storage.read("autistmask");
|
||||
expect(stored.theme).toBe("dark");
|
||||
expect(stored.networkId).toBe("mainnet");
|
||||
});
|
||||
|
||||
test("a wallet added during the earlier save's read is stored", async () => {
|
||||
const storage = makeStorageStub({ autistmask: { wallets: [W1] } });
|
||||
const { state, saveState, loadState } = loadPage(storage).state;
|
||||
await loadState();
|
||||
|
||||
let queued;
|
||||
runInside(storage.get, () => {
|
||||
state.wallets.push(W2);
|
||||
queued = saveState();
|
||||
});
|
||||
await saveState();
|
||||
await queued;
|
||||
|
||||
const stored = storage.read("autistmask");
|
||||
expect(stored.wallets.map((w) => w.encryptedSecret)).toEqual([
|
||||
"secret-one",
|
||||
"secret-two",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a wallet added during the earlier save's write is stored", async () => {
|
||||
const storage = makeStorageStub({ autistmask: { wallets: [W1] } });
|
||||
const { state, saveState, loadState } = loadPage(storage).state;
|
||||
await loadState();
|
||||
|
||||
let queued;
|
||||
runInside(storage.set, () => {
|
||||
state.wallets.push(W2);
|
||||
queued = saveState();
|
||||
});
|
||||
await saveState();
|
||||
await queued;
|
||||
|
||||
const stored = storage.read("autistmask");
|
||||
expect(stored.wallets.map((w) => w.encryptedSecret)).toEqual([
|
||||
"secret-one",
|
||||
"secret-two",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user