2 Commits
Author SHA1 Message Date
sneak dbdc416e77 fix: discard-dist-on-failure keeps the step's status when its message cannot be written (closes #342)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
With stderr closed, the wrapper's message write failed and set -e ended it
with status 2; with stderr a pipe whose reader had gone, the write killed it
with 141. The message is now written with SIGPIPE ignored and its failure
ignored, after the step has run. An interrupt while a step runs now exits with
130 once the step has ended, removing nothing: under bash, a step that caught
the interrupt and exited with a status, as check-censored does, used to get
dist/ removed. A failed check-censored --require-dist still removes dist/. The
header states both. Also the README bullet's missing period.

Model: opus-5-5
2026-10-07 01:43:43 +00:00
clawbot 1cfd69e72d fix: Back from Settings no longer lands on a delete wallet screen left by the gear (closes #480)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Leaving the delete wallet or lost-password screen drops its wallet
selection, but the settings gear had just pushed the screen onto the Back
stack, so Back from Settings showed a screen whose button could only
answer "No wallet selected for deletion." Each screen's leave handler now
also takes it off the top of the stack, as the private key export and
recovery phrase screens do since
#461. Back from Settings then
stays on Settings once, as it does for the recovery phrase screen.

Jest tests drive the gear and then Back on both screens, and the delete
screen's own Back.

Model: opus-5-5
2026-10-07 03:43:08 +02:00
6 changed files with 186 additions and 22 deletions
+7 -1
View File
@@ -277,7 +277,7 @@ provide:
step's exit status, saying on stderr that it did and why. Every step of
`make build` runs through it; `make build-debug` runs none of them through it.
A step that succeeds removes nothing, and a removal that cannot be completed
is reported as loudly as one that was
is reported as loudly as one that was.
- `script/test-verify-build` — exercise every failure mode of
`script/verify-build` against a fixture tree in a temp dir, asserting the exit
status and the message of each, assert the state of `dist/` on disk after a
@@ -1838,6 +1838,9 @@ view would leave a wallet one click from deletion.
try again." on the error line, nothing deleted
- "I have lost my password" → **DeleteWalletLostPassword**
- "Back" → previous screen (Settings)
- Settings gear → **Settings**, whose "Back" never lands back on this
screen: leaving drops the wallet the screen was showing, so it also takes
the screen off the Back stack
#### DeleteWalletLostPassword (`delete-wallet-lost-password`)
@@ -1876,6 +1879,9 @@ view would leave a wallet one click from deletion.
selection comes back with it. The two delete screens are siblings rather
than parent and child: nothing is pushed on the way here, so both have
Settings as their Back target.
- Settings gear → **Settings**, whose "Back" never lands back on this
screen: leaving drops the wallet the screen was showing, so it also takes
the screen off the Back stack
- **Deliberately not password-gated.** A password in front of _discarding_ a
secret protects nobody: an attacker at the popup who wants the wallet gone can
uninstall the extension, so the only person such a gate stops is the owner who
+23
View File
@@ -45,6 +45,29 @@ but the review is broader than any of them.
# Completed Steps
- 2026-10-07: `script/discard-dist-on-failure` returns the failed step's own
exit status even when it cannot write its message, to a closed stderr or to a
pipe nobody reads any more
([#342](https://git.eeqj.de/sneak/AutistMask/issues/342)); it used to return 2
or 141 instead. An interrupt while a step runs now removes nothing and says
nothing whichever shell `/bin/sh` is, even when the step catches it and exits
with a status of its own, as `script/check-censored` does: the wrapper exits
with 130 once the step has ended. Under bash such a step used to have `dist/`
removed. A failed `check-censored --require-dist` still removes `dist/` like
any other step, because a `dist/` not cleared of the name `RULES.md` bars must
not ship. The header states both. `script/test-verify-build` runs the wrapper
with stderr closed and with stderr a pipe nobody reads, and interrupts it
under dash and under bash.
- 2026-10-06: Back from Settings no longer lands on the delete wallet or
lost-password screen after either was left by the settings gear
([#480](https://git.eeqj.de/sneak/AutistMask/issues/480)), the defect
[#461](https://git.eeqj.de/sneak/AutistMask/issues/461) fixed for the two
secret screens. Leaving drops the screen's wallet selection, so its leave
handler now also takes it off the Back stack, as a reopened popup already
does. `tests/deleteWalletLostPassword.test.js` drives the gear and then Back
on both screens.
- 2026-10-06: `script/bootstrap` no longer reports success while node cannot
find a package listed in `dependencies` or `devDependencies` of `package.json`
([#263](https://git.eeqj.de/sneak/AutistMask/issues/263)). After the install
+23 -16
View File
@@ -3,22 +3,21 @@
# step fails, remove dist/ before returning its exit status. Our own extension
# to scripts-to-rule-them-all, wrapped around every step of make build.
#
# Why: with AUTISTMASK_DEBUG=1 exported in the calling shell, make build
# compiles a debug bundle and then fails on it in script/verify-build — but the
# bundle is already written. It is loadable, and every wallet it creates gets
# the publicly committed test recovery phrase from src/shared/constants.js. A
# failed release build that leaves that behind is a smaller version of the trap
# the verifier exists to close, and "the failure was loud" only works on an
# operator who does not load dist/chrome/ anyway. Removing the artifact does not
# depend on that.
# Why: with AUTISTMASK_DEBUG=1 exported, make build compiles a debug bundle and
# then fails on it in script/verify-build, after the bundle is written. It is
# loadable, and every wallet it creates gets the publicly committed test
# recovery phrase from src/shared/constants.js, so a failed build must not leave
# it behind. The removal is never silent: it says on stderr that dist/ is gone
# and why.
#
# Two things this deliberately does not do. It does not wrap make build-debug: a
# debug build that failed is not a mistakable artifact, and its output is the
# evidence of what went wrong. And it never removes anything on a step that
# SUCCEEDS, including the final check-censored --require-dist pass.
#
# The removal is never silent: it says dist/ is gone and why, on stderr, above
# the build's own failure.
# A failed check-censored --require-dist removes dist/ like any other step: a
# dist/ not cleared of the name RULES.md bars must not ship either. A step that
# succeeds removes nothing. An interrupt (Ctrl-C) while a step runs removes
# nothing and says nothing, even when the step catches it and exits with a
# status of its own: the wrapper exits with 130 once the step has ended. An
# interrupt is not a build failure, and whoever interrupted the build knows it
# did not finish. make build-debug is not wrapped: a debug build that failed is
# not a mistakable artifact, and its output is the evidence of what went wrong.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -66,12 +65,20 @@ main() {
exit 1
}
# An interrupt is not a build failure: once the step has ended, exit
# without removing anything, even if the step caught the interrupt and
# exited with a status of its own. bash as /bin/sh would otherwise carry on.
trap 'exit 130' INT
_status=0
"$@" || _status=$?
[ "$_status" -ne 0 ] || return 0
discard_dist
# A message that cannot be written, to a closed stderr or to a pipe nobody
# reads any more, must not replace the step's status.
trap '' PIPE
discard_dist || true
exit "$_status"
}
+70
View File
@@ -915,6 +915,76 @@ run_cases() {
discard_case "the wrapper given no command removes nothing" \
c_control 1 kept "no command given" ""
# With stderr closed the wrapper cannot write its message, and must still
# remove dist/ and return the step's own status.
build_fixture
_status=0
(cd "$FIXTURE" &&
"$FIXTURE/script/discard-dist-on-failure" sh -c 'exit 6' 2>&-) ||
_status=$?
if [ "$_status" -eq 6 ] && [ ! -e "$FIXTURE/dist" ]; then
PASSED=$((PASSED + 1))
echo " ok: a failed step's status survives a closed stderr"
else
FAILED=$((FAILED + 1))
echo " FAIL: a failed step's status survives a closed stderr"
echo " exit status $_status, wanted 6, and dist/ must be gone"
fi
# The same with stderr a pipe nobody reads any more, where the write would
# kill the wrapper with SIGPIPE. The FIFO's only reader opens it, exits and
# is waited for before the wrapper runs, so the pipe never has a reader.
build_fixture
mkfifo "$WORK/stderr-fifo"
_status=0
(
: <"$WORK/stderr-fifo" &
exec 3>"$WORK/stderr-fifo"
wait "$!"
cd "$FIXTURE" &&
"$FIXTURE/script/discard-dist-on-failure" sh -c 'exit 6' 2>&3
) || _status=$?
if [ "$_status" -eq 6 ] && [ ! -e "$FIXTURE/dist" ]; then
PASSED=$((PASSED + 1))
echo " ok: a failed step's status survives a pipe nobody reads"
else
FAILED=$((FAILED + 1))
echo " FAIL: a failed step's status survives a pipe nobody reads"
echo " exit status $_status, wanted 6, and dist/ must be gone"
fi
# An interrupt while a step runs removes nothing and says nothing, even
# when the step catches it and exits with a status of its own, as
# script/check-censored does. The step interrupts the wrapper and then
# itself, as Ctrl-C interrupts every process of the build at once. Run
# under dash and under bash, which /bin/sh may each be: bash carries on
# after such a step unless the wrapper stops it.
for _shell in dash bash; do
_name="an interrupt under $_shell removes nothing"
if ! command -v "$_shell" >/dev/null 2>&1; then
SKIPPED=$((SKIPPED + 1))
SKIPPED_NAMES="$SKIPPED_NAMES## - $_name ($_shell not found)$NEWLINE"
echo " SKIP ($_shell not found): $_name"
continue
fi
build_fixture
_status=0
_out="$(cd "$FIXTURE" && "$_shell" \
"$FIXTURE/script/discard-dist-on-failure" \
sh -c 'trap "exit 4" INT; kill -INT "$PPID" $$; exit 5' 2>&1)" ||
_status=$?
if [ "$_status" -eq 130 ] && [ -z "$_out" ] &&
[ -f "$FIXTURE/dist/chrome/src/popup/index.js" ]; then
PASSED=$((PASSED + 1))
echo " ok: $_name"
else
FAILED=$((FAILED + 1))
echo " FAIL: $_name"
echo " exit status $_status, wanted 130; dist/ must be intact" \
"and nothing said. Output: $_out"
fi
done
check_makefile_wiring
}
+22 -5
View File
@@ -49,9 +49,9 @@ function confirmKey(name) {
}
// Drop the password from the DOM and the wallet selection from the
// closure. Registered as the view-leave handler as well as run on entry,
// so the typed password does not sit in the hidden view after the user
// navigates away by any route, including the Settings gear.
// closure. Run by the view-leave handler as well as on entry, so the typed
// password does not sit in the hidden view after the user navigates away
// by any route, including the Settings gear.
function clear() {
deleteWalletIndex = null;
$("delete-wallet-password").value = "";
@@ -147,8 +147,25 @@ async function finishDelete(walletIdx) {
function init(_ctx) {
ctx = _ctx;
onViewLeave("delete-wallet-confirm", clear);
onViewLeave("delete-wallet-lost-password", clearLostPassword);
// Leaving drops the wallet selection, so each screen also comes off the
// Back stack, where the settings gear has just put it: Back from
// Settings must not land on a screen whose button can only answer "No
// wallet selected for deletion." A reopened popup drops them from the
// stack the same way (https://git.eeqj.de/sneak/AutistMask/issues/480).
onViewLeave("delete-wallet-confirm", () => {
clear();
const stack = state.viewStack;
if (stack[stack.length - 1] === "delete-wallet-confirm") {
stack.pop();
}
});
onViewLeave("delete-wallet-lost-password", () => {
clearLostPassword();
const stack = state.viewStack;
if (stack[stack.length - 1] === "delete-wallet-lost-password") {
stack.pop();
}
});
// No wipe here: goBack() routes through showView(), which runs the
// leave hook.
+41
View File
@@ -615,3 +615,44 @@ describe("the password route's confirm button", () => {
]);
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/480: leaving either delete
// screen drops its wallet selection, so Back onto one showed a screen whose
// button could only answer "No wallet selected for deletion."
describe("Back from Settings after leaving by the settings gear", () => {
test("does not land on the delete screen", () => {
const { helpers, deleteWallet, state } = load();
deleteWallet.show(1);
// The settings gear: push the current view, then show Settings.
helpers.pushCurrentView();
helpers.showView("settings");
expect(state.viewStack).toEqual(["main", "settings"]);
helpers.goBack();
expect(state.currentView).not.toBe("delete-wallet-confirm");
});
test("does not land on the lost-password screen", async () => {
const { helpers, deleteWallet, state } = load();
await openLostPassword(deleteWallet, 1);
// The settings gear: push the current view, then show Settings.
helpers.pushCurrentView();
helpers.showView("settings");
expect(state.viewStack).toEqual(["main", "settings"]);
helpers.goBack();
expect(state.currentView).not.toBe(VIEW);
});
// The lost-password screen's own Back is "Back returns to the delete
// screen with its wallet still chosen", above.
test("the delete screen's own Back leaves the rest of the stack alone", async () => {
const { deleteWallet, state } = load();
deleteWallet.show(1);
await click("btn-delete-wallet-back");
expect(state.currentView).toBe("settings");
expect(state.viewStack).toEqual(["main"]);
});
});