test: cover every verify-build failure mode from make check (closes #227) #249

Merged
clawbot merged 1 commits from test/issue-227-verify-build-battery into next 2026-08-12 11:05:09 +02:00
Collaborator

Closes #227.

script/test-verify-build turns the hand battery into a committed target, run from script/check. 18 cases, each asserting the exit status AND a substring of the message — a guard that fails for the wrong reason is a defect this harness catches, and it did (see the break demo below).

Fixture

mktemp -d, holding script/verify-build as a symlink to the real script plus a synthetic dist/. verify-build derives ROOT from dirname "$0"/.., so it runs entirely against the fixture and never reads or writes the repo's build output. The symlink rather than a copy is what makes a break in the live script fail here. TMPDIR is pointed inside the work dir, so verify-build's own mktemp also leaves nothing behind; an EXIT/INT/TERM trap removes the tree, chmod -R u+rwX first because the cases deliberately create unreadable files.

Cases: control, trailing space, embedded newline, dist/ as a symlink, unwalkable subtree, dangling symlink, directory-pointing symlink, symlink to a listed bundle under an unlisted path, missing/empty/unreadable manifest, missing/empty/unreadable listed bundle, unlisted extension, no marker, both markers, wrong marker.

The root/permission problem, and which resolution this is

Neither of the issue's two options verbatim: a strict superset of the second, because a case that is permanently skipped in CI gates nothing, and CI is the only place this runs unattended.

The harness picks a runner for the three permission-dependent cases and then proves the runner in that very run before counting them, with three probes:

  • a mode-644 file must be readable through it, and
  • script/verify-build itself must be readable through it — else the runner is broken or cannot reach the script, and the cases would fail for the wrong reason;
  • a mode-000 file must NOT be readable through it — else permissions are not in force and the cases would pass without testing anything.

Unprivileged: the runner is "run directly" and the probes are about this process. As root: setpriv --reuid=65534 then runuser -u nobody, both present in the pinned node:22-slim CI image, so the cases really run in CI (verified below). Only when no candidate passes all three probes are those cases skipped — and then a banner names each skipped case and the final line reads SKIPPED AND NOT PROVEN instead of passed. A false green is impossible by construction: either permission enforcement was demonstrated against a mode-000 file in this run, or the output says the cases did not run.

Why not a non-root USER in the Dockerfile, the "honest" option: COPY . . and RUN script/bootstrap leave /app and node_modules root-owned, and RUN make build writes dist/, so a non-root USER needs a chown -R /app layer duplicating the entire node_modules tree in the image. That is a CI-container change with a real size cost, and outside this issue. Happy to file it separately if wanted.

Proof both branches behave, in the pinned CI image as uid 0:

$ docker run --rm -v ...:/app:ro -w /app node@sha256:5373f19...43fc36 sh -c 'id -u; script/test-verify-build; ls -A /tmp'
0
Testing script/verify-build failure modes...
  permission cases: enabled (runner: setpriv, proved against a mode-000 file)
  ...
test-verify-build: 18 case(s) passed
EXIT=0
--- residue in /tmp:            <- empty

Same image with setpriv and runuser moved aside:

  SKIP (permissions not in force): manifest unreadable
  ...
################################################################################
## WARNING: 3 PERMISSION CASE(S) DID NOT RUN, AND THIS RUN DOES NOT
## PROVE THEM. This process is uid 0, and no runner subject to file
## permissions was available. Tried: setpriv, runuser.
## Under root, chmod 000 stops neither find nor grep, so these cases would
## have passed without testing anything. They were skipped, not counted:
##   - unwalkable subtree under dist/
##   - manifest unreadable
##   - listed bundle unreadable
################################################################################
test-verify-build: 15 case(s) passed, 3 SKIPPED AND NOT PROVEN (see the warning above)

Deliberate break, demonstrated

has_marker's exit-2 branch in script/verify-build changed from fail to return 1 — the historical grep exit-2 conflation. Reverted after capture; the committed script is unmodified.

Testing script/verify-build failure modes...
  permission cases: enabled (runner: direct, proved against a mode-000 file)
  ok: control: untouched dist passes
  ok: unlisted marker-carrying file, trailing space in name
  ok: unlisted marker-carrying file, newline in name
  ok: dist/ replaced by a symlink
  ok: unwalkable subtree under dist/
  FAIL: dangling symlink under dist/
    exit status 0, wanted 1; message did not contain: reading dist/chrome/dangling.js, so the file could not be
    --- verify-build output ---
    Verifying emitted bundles (expecting autistmask-build-debug=off)...
      ok: dist/chrome/src/popup/index.js (autistmask-build-debug=off)
      ok: dist/firefox/src/popup/index.js (autistmask-build-debug=off)
    grep: dist/chrome/dangling.js: No such file or directory
    verify-build: 2 bundle(s) verified autistmask-build-debug=off
    --- end output ---
  FAIL: symlink to a directory under dist/
    exit status 0, wanted 1; message did not contain: reading dist/chrome/link-to-dir, so the file could not be
    --- verify-build output ---
    ...
    grep: dist/chrome/link-to-dir: Is a directory
    verify-build: 2 bundle(s) verified autistmask-build-debug=off
    --- end output ---
  ok: symlink to a listed bundle under an unlisted path
  ok: manifest missing
  ok: manifest empty
  ok: manifest unreadable
  ok: listed bundle missing
  ok: listed bundle empty
  FAIL: listed bundle unreadable
    message did not contain: reading dist/chrome/src/popup/index.js, so the file could not be
    --- verify-build output ---
    Verifying emitted bundles (expecting autistmask-build-debug=off)...
    grep: dist/chrome/src/popup/index.js: Permission denied
    verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
        determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js
        or the emitted output changed shape. Refusing to report success.
    --- end output ---
  ok: unlisted extension carrying a marker
  ok: listed bundle carries no marker
  ok: listed bundle carries both markers
  ok: wrong marker for the requested mode
test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped

Exit 1, so make check fails. Note the third failure: that break leaves the exit status at 1, and only the message assertion catches it — the guard still fails, but it now blames the bundle for a permissions fault. That is exactly the wrong-reason defect the issue asked this harness to catch, and status-only assertions would have passed it.

Verification

  • make check green on this branch rebased onto next at afe6dda: 21 test suites / 443 tests, 18 battery cases, prettier clean.
  • The deliberate break above was re-run against this rebased head: make check exits non-zero, test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped. Restored, green again, git status clean.
  • docker build --no-cache . exit 0 — RUN make check executed uncached, with permission cases: enabled (runner: setpriv, proved against a mode-000 file) and test-verify-build: 18 case(s) passed in the layer log, so the permission cases run rather than skip in CI.
  • No residue: nothing matching autistmask-*/verify-build* left in /tmp, no dist/ created, git status clean after runs.

Runtime cost

0.705s locally, 0.32s in the CI container. Each case rebuilds a 7-file fixture and runs verify-build once; nothing is compiled or installed.

Closes [#227](https://git.eeqj.de/sneak/AutistMask/issues/227). `script/test-verify-build` turns the hand battery into a committed target, run from `script/check`. 18 cases, each asserting the exit status AND a substring of the message — a guard that fails for the wrong reason is a defect this harness catches, and it did (see the break demo below). ## Fixture `mktemp -d`, holding `script/verify-build` as a **symlink to the real script** plus a synthetic `dist/`. `verify-build` derives `ROOT` from `dirname "$0"/..`, so it runs entirely against the fixture and never reads or writes the repo's build output. The symlink rather than a copy is what makes a break in the live script fail here. `TMPDIR` is pointed inside the work dir, so `verify-build`'s own `mktemp` also leaves nothing behind; an `EXIT`/`INT`/`TERM` trap removes the tree, `chmod -R u+rwX` first because the cases deliberately create unreadable files. Cases: control, trailing space, embedded newline, `dist/` as a symlink, unwalkable subtree, dangling symlink, directory-pointing symlink, symlink to a listed bundle under an unlisted path, missing/empty/unreadable manifest, missing/empty/unreadable listed bundle, unlisted extension, no marker, both markers, wrong marker. ## The root/permission problem, and which resolution this is Neither of the issue's two options verbatim: a strict superset of the second, because a case that is permanently skipped in CI gates nothing, and CI is the only place this runs unattended. The harness picks a runner for the three permission-dependent cases and then **proves the runner in that very run before counting them**, with three probes: - a mode-644 file must be readable through it, and - `script/verify-build` itself must be readable through it — else the runner is broken or cannot reach the script, and the cases would fail for the wrong reason; - a mode-000 file must NOT be readable through it — else permissions are not in force and the cases would pass without testing anything. Unprivileged: the runner is "run directly" and the probes are about this process. As root: `setpriv --reuid=65534` then `runuser -u nobody`, both present in the pinned `node:22-slim` CI image, so the cases really run in CI (verified below). Only when no candidate passes all three probes are those cases skipped — and then a banner names each skipped case and the final line reads `SKIPPED AND NOT PROVEN` instead of passed. A false green is impossible by construction: either permission enforcement was demonstrated against a mode-000 file in this run, or the output says the cases did not run. Why not a non-root `USER` in the Dockerfile, the "honest" option: `COPY . .` and `RUN script/bootstrap` leave `/app` and `node_modules` root-owned, and `RUN make build` writes `dist/`, so a non-root `USER` needs a `chown -R /app` layer duplicating the entire `node_modules` tree in the image. That is a CI-container change with a real size cost, and outside this issue. Happy to file it separately if wanted. Proof both branches behave, in the pinned CI image as uid 0: ``` $ docker run --rm -v ...:/app:ro -w /app node@sha256:5373f19...43fc36 sh -c 'id -u; script/test-verify-build; ls -A /tmp' 0 Testing script/verify-build failure modes... permission cases: enabled (runner: setpriv, proved against a mode-000 file) ... test-verify-build: 18 case(s) passed EXIT=0 --- residue in /tmp: <- empty ``` Same image with `setpriv` and `runuser` moved aside: ``` SKIP (permissions not in force): manifest unreadable ... ################################################################################ ## WARNING: 3 PERMISSION CASE(S) DID NOT RUN, AND THIS RUN DOES NOT ## PROVE THEM. This process is uid 0, and no runner subject to file ## permissions was available. Tried: setpriv, runuser. ## Under root, chmod 000 stops neither find nor grep, so these cases would ## have passed without testing anything. They were skipped, not counted: ## - unwalkable subtree under dist/ ## - manifest unreadable ## - listed bundle unreadable ################################################################################ test-verify-build: 15 case(s) passed, 3 SKIPPED AND NOT PROVEN (see the warning above) ``` ## Deliberate break, demonstrated `has_marker`'s exit-2 branch in `script/verify-build` changed from `fail` to `return 1` — the historical grep exit-2 conflation. Reverted after capture; the committed script is unmodified. ``` Testing script/verify-build failure modes... permission cases: enabled (runner: direct, proved against a mode-000 file) ok: control: untouched dist passes ok: unlisted marker-carrying file, trailing space in name ok: unlisted marker-carrying file, newline in name ok: dist/ replaced by a symlink ok: unwalkable subtree under dist/ FAIL: dangling symlink under dist/ exit status 0, wanted 1; message did not contain: reading dist/chrome/dangling.js, so the file could not be --- verify-build output --- Verifying emitted bundles (expecting autistmask-build-debug=off)... ok: dist/chrome/src/popup/index.js (autistmask-build-debug=off) ok: dist/firefox/src/popup/index.js (autistmask-build-debug=off) grep: dist/chrome/dangling.js: No such file or directory verify-build: 2 bundle(s) verified autistmask-build-debug=off --- end output --- FAIL: symlink to a directory under dist/ exit status 0, wanted 1; message did not contain: reading dist/chrome/link-to-dir, so the file could not be --- verify-build output --- ... grep: dist/chrome/link-to-dir: Is a directory verify-build: 2 bundle(s) verified autistmask-build-debug=off --- end output --- ok: symlink to a listed bundle under an unlisted path ok: manifest missing ok: manifest empty ok: manifest unreadable ok: listed bundle missing ok: listed bundle empty FAIL: listed bundle unreadable message did not contain: reading dist/chrome/src/popup/index.js, so the file could not be --- verify-build output --- Verifying emitted bundles (expecting autistmask-build-debug=off)... grep: dist/chrome/src/popup/index.js: Permission denied verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js or the emitted output changed shape. Refusing to report success. --- end output --- ok: unlisted extension carrying a marker ok: listed bundle carries no marker ok: listed bundle carries both markers ok: wrong marker for the requested mode test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped ``` Exit 1, so `make check` fails. Note the third failure: that break leaves the exit status at 1, and only the message assertion catches it — the guard still fails, but it now blames the bundle for a permissions fault. That is exactly the wrong-reason defect the issue asked this harness to catch, and status-only assertions would have passed it. ## Verification - `make check` green on this branch rebased onto `next` at `afe6dda`: 21 test suites / 443 tests, 18 battery cases, prettier clean. - The deliberate break above was re-run against this rebased head: `make check` exits non-zero, `test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped`. Restored, green again, `git status` clean. - `docker build --no-cache .` exit 0 — `RUN make check` executed uncached, with `permission cases: enabled (runner: setpriv, proved against a mode-000 file)` and `test-verify-build: 18 case(s) passed` in the layer log, so the permission cases run rather than skip in CI. - No residue: nothing matching `autistmask-*`/`verify-build*` left in `/tmp`, no `dist/` created, `git status` clean after runs. ## Runtime cost 0.705s locally, 0.32s in the CI container. Each case rebuilds a 7-file fixture and runs `verify-build` once; nothing is compiled or installed.
clawbot added 1 commit 2026-08-12 10:27:42 +02:00
test: cover every verify-build failure mode from make check (closes #227)
All checks were successful
check / check (push) Successful in 52s
ef349e8a39
script/verify-build is the build-integrity guard and nothing in make check
tested it. Three separate reviews of it each found a fresh vacuous pass --
the grep exit-2 conflation, the discarded find status, the line-delimited
walk -- and each was caught by someone building a tree by hand.

script/test-verify-build makes that battery a target. It builds a fixture
tree under mktemp -d holding script/verify-build as a symlink to the real
script: verify-build takes its ROOT from dirname "$0"/.., so it runs against
the fixture's dist/ and never touches the repo's build output, and the
symlink rather than a copy is what makes a break in the real script fail
here. Its own mktemp is pointed inside the work dir, so a run leaves nothing
behind. Each of the 18 cases asserts the exit status AND a substring of the
message, because a guard that fails for the wrong reason is itself a defect.

The obstacle was that the CI image declares no USER, so CI runs as root, and
root is not subject to file permissions: a chmod 000 case passes vacuously
there while passing honestly on a developer machine. The harness therefore
proves its runner before counting the five permission-dependent cases -- a
mode-644 file and script/verify-build itself must be readable through it, a
mode-000 file must not be -- and drops to uid 65534 via setpriv or runuser
when running as root, both present in the pinned base image. If no candidate
passes the probes the cases are skipped, named in a banner, and the final
line says SKIPPED AND NOT PROVEN rather than passed. A green run cannot mean
"the permission cases did not run" without saying so.
clawbot added the needs-review label 2026-08-12 10:27:49 +02:00
clawbot self-assigned this 2026-08-12 10:27:50 +02:00
Author
Collaborator

FAIL — needs-rebase.

1. Does not rebase onto current next (blocking)

origin/next has moved to ce4a0d7; this head (ef349e8) is 1 ahead / 2 behind and conflicts. Gitea's mergeable: true was computed against ba35282 and is stale.

$ git rebase origin/next
Auto-merging README.md
Auto-merging TODO.md
CONFLICT (content): Merge conflict in TODO.md
error: could not apply ef349e8... test: cover every verify-build failure mode (closes #227)

TODO.md: this commit and #230 both insert a bullet at the top of # Completed Steps. Acceptable: rebase onto ce4a0d7, keep both bullets (this one dated 2026-08-12, no landed entry dropped), re-run make check, force-push.

2. Commit message overstates the permission coverage (fix in the same rebase)

The commit body says "before counting the five permission-dependent cases"; the PR body and the plan comment on #227 repeat it. There are three, at script/test-verify-build:339, :366, :378grep -c 'yes release' script/test-verify-build returns 3, and the PR's own captured skip output says 3 PERMISSION CASE(S) DID NOT RUN. This lands in permanent history and overstates by 67% the coverage that the runner-proving machinery buys. Acceptable: say "three" in the commit body (and correct the PR body).

Independent break results (the central claim holds)

Eleven guards in script/verify-build broken one at a time in a scratch copy; the battery caught ten, exit 1 each time:

break caught by
find status check dropped unwalkable subtree
walk back to -print + while read trailing space + newline
is_listed newline guard dropped newline in name
manifest-missing returns 0 silently manifest missing
-type l dropped from find 3 symlink cases
[ "$MARKER" = "$expected" ] weakened wrong marker
[ -h dist ] guard dropped dist symlink — message only, status stayed 1
[ -s "$file" ] -> -e listed bundle empty — message only
has_marker exit-2 -> return 1 3 cases, one message-only (reproduces the PR's demo exactly)
[ -r "$MANIFEST" ] -> -e manifest unreadable

Two of my breaks beyond the author's were caught only by the message assertion with the exit status still correct — the wrong-reason defect class the issue asked for, independently confirmed.

Not caught: is_listed's exit-2 branch (script/verify-build:98). Changing it to return 1 leaves all 18 cases green, because main's [ -r "$MANIFEST" ] fires first and no fixture state reaches it. Not on the issue's case list and effectively unreachable while that guard stands — noting it, not blocking on it.

Root/permission resolution: verified by execution, not accepted on the description

In the pinned node:22-slim as uid 0 the cases run, not skip (runner: setpriv), and they are not vacuous under it: with the find-status, -r-manifest and has_marker guards broken simultaneously, all five permission-touching cases fail as root in the container (5 case(s) FAILED, 13 passed, 0 skipped). Forcing the skip path (both helpers moved aside) yields the banner naming each case and 3 SKIPPED AND NOT PROVEN, exit 0 — green-with-banner is the issue's sanctioned option 2, and it is unreachable in the pinned image. A broken environment (unreachable $WORK, no helper) fails the probe and skips loudly rather than false-greening.

Everything else checked and clean

Coverage matches the issue list exactly (18/18, no stubs); all 18 assert a message, only one substring shared between two cases and both fail independently under the line-delimited break; symlink fixture resolves ROOT into the temp tree; no dist/, no /tmp residue, repo tree and script/verify-build mode unchanged after runs; POSIX clean (dash and busybox ash parse and run it, shellcheck -s sh silent); make check 14.0s locally with the battery at 0.78s; script/cibuild rc=0 with RUN make check executed (26.1s, not CACHED) showing 14 suites / 361 tests and 18 case(s) passed; tracker CI green on ef349e8; single commit, author and committer clawbot, title ends (closes #227); one TODO.md bullet, pure addition; README Entrypoints updated; make fmt clean; no scope creep; no attribution trailers; no competitor named.

One portability note, not a defect in this PR: under busybox grep the "symlink to a directory" case fails, because busybox exits 1 on a directory where GNU exits 2 — the exact fragility check_unlisted_bundles' own comment predicts. Pre-existing in verify-build, surfaced rather than caused by this harness, and irrelevant to the GNU-grep CI image.

FAIL — `needs-rebase`. ## 1. Does not rebase onto current `next` (blocking) `origin/next` has moved to `ce4a0d7`; this head (`ef349e8`) is 1 ahead / 2 behind and conflicts. Gitea's `mergeable: true` was computed against `ba35282` and is stale. ``` $ git rebase origin/next Auto-merging README.md Auto-merging TODO.md CONFLICT (content): Merge conflict in TODO.md error: could not apply ef349e8... test: cover every verify-build failure mode (closes #227) ``` `TODO.md`: this commit and [#230](https://git.eeqj.de/sneak/AutistMask/issues/230) both insert a bullet at the top of `# Completed Steps`. Acceptable: rebase onto `ce4a0d7`, keep both bullets (this one dated `2026-08-12`, no landed entry dropped), re-run `make check`, force-push. ## 2. Commit message overstates the permission coverage (fix in the same rebase) The commit body says "before counting **the five** permission-dependent cases"; the PR body and the plan comment on [#227](https://git.eeqj.de/sneak/AutistMask/issues/227#issuecomment-57836) repeat it. There are **three**, at `script/test-verify-build:339`, `:366`, `:378` — `grep -c 'yes release' script/test-verify-build` returns 3, and the PR's own captured skip output says `3 PERMISSION CASE(S) DID NOT RUN`. This lands in permanent history and overstates by 67% the coverage that the runner-proving machinery buys. Acceptable: say "three" in the commit body (and correct the PR body). ## Independent break results (the central claim holds) Eleven guards in `script/verify-build` broken one at a time in a scratch copy; the battery caught **ten**, exit 1 each time: | break | caught by | |---|---| | `find` status check dropped | unwalkable subtree | | walk back to `-print` + `while read` | trailing space + newline | | `is_listed` newline guard dropped | newline in name | | manifest-missing returns 0 silently | manifest missing | | `-type l` dropped from `find` | 3 symlink cases | | `[ "$MARKER" = "$expected" ]` weakened | wrong marker | | `[ -h dist ]` guard dropped | dist symlink — **message only**, status stayed 1 | | `[ -s "$file" ]` -> `-e` | listed bundle empty — **message only** | | `has_marker` exit-2 -> `return 1` | 3 cases, one message-only (reproduces the PR's demo exactly) | | `[ -r "$MANIFEST" ]` -> `-e` | manifest unreadable | Two of my breaks beyond the author's were caught **only** by the message assertion with the exit status still correct — the wrong-reason defect class the issue asked for, independently confirmed. Not caught: `is_listed`'s exit-2 branch (`script/verify-build:98`). Changing it to `return 1` leaves all 18 cases green, because `main`'s `[ -r "$MANIFEST" ]` fires first and no fixture state reaches it. Not on the issue's case list and effectively unreachable while that guard stands — noting it, not blocking on it. ## Root/permission resolution: verified by execution, not accepted on the description In the pinned `node:22-slim` as uid 0 the cases **run**, not skip (`runner: setpriv`), and they are **not vacuous** under it: with the `find`-status, `-r`-manifest and `has_marker` guards broken simultaneously, all five permission-touching cases fail as root in the container (`5 case(s) FAILED, 13 passed, 0 skipped`). Forcing the skip path (both helpers moved aside) yields the banner naming each case and `3 SKIPPED AND NOT PROVEN`, exit 0 — green-with-banner is the issue's sanctioned option 2, and it is unreachable in the pinned image. A broken environment (unreachable `$WORK`, no helper) fails the probe and skips loudly rather than false-greening. ## Everything else checked and clean Coverage matches the issue list exactly (18/18, no stubs); all 18 assert a message, only one substring shared between two cases and both fail independently under the line-delimited break; symlink fixture resolves `ROOT` into the temp tree; no `dist/`, no `/tmp` residue, repo tree and `script/verify-build` mode unchanged after runs; POSIX clean (`dash` and `busybox ash` parse and run it, shellcheck `-s sh` silent); `make check` 14.0s locally with the battery at 0.78s; `script/cibuild` rc=0 with `RUN make check` **executed** (26.1s, not `CACHED`) showing 14 suites / 361 tests and `18 case(s) passed`; tracker CI green on `ef349e8`; single commit, author and committer `clawbot`, title ends ` (closes #227)`; one `TODO.md` bullet, pure addition; README Entrypoints updated; `make fmt` clean; no scope creep; no attribution trailers; no competitor named. One portability note, not a defect in this PR: under **busybox** `grep` the "symlink to a directory" case fails, because busybox exits 1 on a directory where GNU exits 2 — the exact fragility `check_unlisted_bundles`' own comment predicts. Pre-existing in `verify-build`, surfaced rather than caused by this harness, and irrelevant to the GNU-grep CI image.
clawbot added needs-rebase and removed needs-review labels 2026-08-12 10:40:11 +02:00
clawbot force-pushed test/issue-227-verify-build-battery from ef349e8a39 to 07780c3d8c 2026-08-12 11:03:29 +02:00 Compare
clawbot merged commit 78a1cb067e into next 2026-08-12 11:05:09 +02:00
clawbot deleted branch test/issue-227-verify-build-battery 2026-08-12 11:05:09 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#249