Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30b59325fa |
@@ -22,10 +22,11 @@ on: [push]
|
|||||||
# These jobs REPORT, they do not gate. Whether a check blocks a merge is
|
# 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
|
# 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
|
# here is a red mark a reviewer has to account for rather than a hard
|
||||||
# block. Making e2e-chrome a required check is blocked while reports of the
|
# block. Making e2e-chrome a required check is blocked on the measured
|
||||||
# Chrome suite failing under load are still open; the "In CI" section of
|
# flake in the dApp signing wait -- two of six runs of unmutated code on a
|
||||||
# README.md names them. A gate that fails at random teaches people to merge
|
# loaded machine -- tracked as
|
||||||
# past red.
|
# https://git.eeqj.de/sneak/AutistMask/issues/287. 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
|
# 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
|
# `|| true`. Both scripts exit non-zero when docker is missing, when the
|
||||||
|
|||||||
@@ -619,13 +619,14 @@ 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. A report of the Chrome suite
|
That is not only a statement about configuration. The Chrome suite is
|
||||||
**failing under load** is still open:
|
**measurably flaky under load** — two of six runs of unmutated code on a busy
|
||||||
[#290](https://git.eeqj.de/sneak/AutistMask/issues/290), runs on a busy machine
|
machine lost the approval popup out from under the dApp signing wait, always in
|
||||||
failing with `the extension opened no approval window within 30000ms`. So a red
|
the `#183` section, tracked as
|
||||||
`e2e-chrome` has to be read before it is believed, and those failures are the
|
[#287](https://git.eeqj.de/sneak/AutistMask/issues/287). So a red `e2e-chrome`
|
||||||
blocker to ever making this a required check. Do not answer them with a retry
|
has to be read before it is believed, and that flake is the blocker to ever
|
||||||
wrapper: a suite that reruns until it is green stops being evidence.
|
making this a required check. Do not answer it 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
|
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
|
||||||
|
|||||||
@@ -51,36 +51,9 @@ but the review is broader than any of them.
|
|||||||
close before the save lands loses the switch; with the network left on
|
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
|
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()`
|
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. The restore
|
waits until the view it expects to reopen on is the saved one. A change made
|
||||||
half of the round trip and the second filter toggle change a setting right
|
while an earlier save from the same page is still running is lost even without
|
||||||
after a reopen, while the reopened popup's own saves may still be running;
|
a close; that is [#448](https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||||
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
|
- 2026-10-05: The lost-password delete confirmation refuses an empty field and
|
||||||
ignores characters that paint nothing
|
ignores characters that paint nothing
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ function releaseApproval(approval) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open approval in a separate popup window, unless it is no longer pending.
|
// Open approval in a separate popup window.
|
||||||
// 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,10 +446,6 @@ 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);
|
||||||
|
|||||||
+5
-12
@@ -122,9 +122,9 @@ function currentNetwork() {
|
|||||||
return networkById(state.networkId);
|
return networkById(state.networkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The persisted fields as this page held them when its last loadState()
|
// The persisted fields as they stood at the end of this page's last
|
||||||
// finished, or when its last successful saveState() began. saveState() diffs
|
// loadState() or saveState(). saveState() diffs the live state against this
|
||||||
// the live state against this to find only the fields THIS page changed since.
|
// to find only the fields THIS page actually changed.
|
||||||
//
|
//
|
||||||
// Deep-cloned, not a reference: callers mutate persisted objects and arrays
|
// Deep-cloned, not a reference: callers mutate persisted objects and arrays
|
||||||
// in place (state.wallets.push(...)), and a reference baseline would mutate
|
// in place (state.wallets.push(...)), and a reference baseline would mutate
|
||||||
@@ -464,10 +464,7 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
// does not own goes on being whatever its last loadState() saw, same as
|
// does not own goes on being whatever its last loadState() saw, same as
|
||||||
// before this fix; only the persisted record is guaranteed current.
|
// before this fix; only the persisted record is guaranteed current.
|
||||||
async function saveStateOnce() {
|
async function saveStateOnce() {
|
||||||
// A copy, so what this save compares and writes is the page's state as it
|
const current = snapshotPersisted();
|
||||||
// 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");
|
const result = await storageGet("autistmask");
|
||||||
// The record in storage right now is about to be merged into and written
|
// 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,
|
// back, so it is validated exactly like a load validates it. Without this,
|
||||||
@@ -524,11 +521,7 @@ async function saveStateOnce() {
|
|||||||
// exactly as it stood; see the note above.
|
// exactly as it stood; see the note above.
|
||||||
rawState.hasWallet = rawState.wallets.length > 0;
|
rawState.hasWallet = rawState.wallets.length > 0;
|
||||||
|
|
||||||
// What this save compared and wrote, not the page's state now: a change
|
baseline = structuredClone(snapshotPersisted());
|
||||||
// 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
|
// showView() calls saveState() on every navigation without awaiting it, so
|
||||||
|
|||||||
@@ -2235,27 +2235,6 @@ 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.
|
||||||
|
|||||||
+2
-4
@@ -2812,7 +2812,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. Both call sites arm the click trace below and assert it.
|
// accepts. That call site arms the click trace below and asserts 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.
|
||||||
@@ -3198,9 +3198,7 @@ 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 armClickTrace(env, popup, "#btn-reject");
|
await popup.click("#btn-reject");
|
||||||
await clickAndClose(popup, "#btn-reject");
|
|
||||||
await assertClickLanded(env, "#btn-reject");
|
|
||||||
|
|
||||||
await assertUserRejection(
|
await assertUserRejection(
|
||||||
phishingDapp,
|
phishingDapp,
|
||||||
|
|||||||
@@ -423,80 +423,3 @@ describe("two wallets independently created with a colliding identity", () => {
|
|||||||
expect(secrets).toContain("secret-b");
|
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