A scene per state, asserted, and the manual generated from it #12

Merged
geusebio merged 4 commits from feature/capture-suite-per-scene into main 2026-08-17 16:52:24 +00:00
Owner

Closes nothing automatically — this is the tray half of the platform repo's
#627, whose other half is a pin bump in that repo. A cross-repo Closes will not fire, so #627 gets closed by hand once both land.

Rebased on merged #11 and #6; #6's VM-capture plan in hack/screenshot/README.md is kept in full, with the dressing paragraph reconciled by hand.

hack/screenshot had one scene, one walk and one golden. It now has nine scenes — one per state a user actually meets — each asserted as text and each illustrated in a manual that is generated from the same run.

What is here

A scene per state hack/screenshot/scenes/<name>/scene.json: what the platform answers, and the walk that photographs it. Merged over scenes/defaults.json, so a scene says only what makes it that scene
Scenes that move through time phases replaces the listing N seconds in, which is the only way to capture a state that exists as a change — an application vanishing between two polls
One golden per scene every dump that scene's walk took, in one file, diffed by check.sh. Blessing is --bless and the diff is the review
A generated manual docs/manual.md, assembled by manual.py out of the scenes and what they captured. CI fails a manual that is not what a rebuild produces, so it cannot drift
A capture job in CI takes the captures for real and fails if they differ from the commit

The nine, in the order the manual meets them: signed out · nothing deployed · the tree and its gestures · an activation followed to its outcome · an activation refused for credit · a deployment that can do neither · an application that went away · one that turned up · the platform not answering.

Three things about the harness that had to change

The walk names rows now; it does not count keypresses. menudump --to 'shop · #12|main · running' reads the exported tree, skips exactly what GTK's keyboard skips, and prints the keystrokes that reach that row — so a row that moved is still reached, a row that is gone fails the run naming the labels it did find, and an ambiguous name fails rather than photographing whichever matched first. This is not tidying: the counted walk had been wrong for two commits. docs/images/*/tray-activate.png on main is a photograph of Open in browser, and tray-gestures.png is billing's Activate rather than the confirming stop it claims — both because the organization became a heading and every walk below it went one row too far. One of them was embedded in the README.

The shutter waits for the screen to stop moving. Two identical consecutive frames, then keep that one. A third-level submenu under a compositor takes an unpredictable moment to paint and the difference between a good capture and a photograph of a half-drawn menu was about half a second, in either direction depending on the scene.

Some rows are clicked over the bus. menudump --click sends dbusmenu's own Event — the same call a real desktop's host makes. The scenes that are about a toggle's consequences use it, because the toggles live one level deeper than this host draws reliably (see #844 below), and what those scenes photograph is the consequence anyway.

cmd/menudump grew tests for all of that: the label→keystroke arithmetic is exactly the kind of logic whose bugs are silent, and it took the package from 0% to 48.2% (floor bumped).

What the captures found — read, not just attached

Policy point 2, doing its job. Five issues filed in the platform repo, none fixed here:

#838 Signed out, sc-tray says so only on a stderr nobody is reading — it refuses to start, and the user-visible state is an empty panel
#839 The organization row's braille sparkline renders as a dotted leader, not a chart — legible as text, punctuation on screen
#841 The status row loses every sentence it is given, to the resync the toggle itself triggers. The N new application(s) — restart sc-tray branch is unreachable dead code, overwritten two statements later in the same function
#844 The third level of the menu draws unreliably, so the deployment toggles sometimes cannot be reached at all. The tray exports the rows correctly — menudump proves it — and the host draws an empty strip
#847 A vanished application keeps live toggles for deployments that no longer exist: update retitles the row and continues past the code that would have hidden them

#844 is the one worth a reviewer's attention. It is unresolved whether it is the app or the harness's host — the honest answer needs a second host (KDE's, or an XFCE panel) and I have not run one. Until it is answered the suite works around it visibly rather than quietly:

  • no goto may go deeper than two levels, and scene.py refuses one that tries, naming the issue;
  • the gesture rows are asserted as text in every golden — label, order, and which of the pair is hidden;
  • the inert scene deliberately has no photograph of its empty submenu, because such a picture is indistinguishable from #844;
  • the manual says all of this where a reader meets it.

Two goldens (in-flight, refused) record the wrong-but-current status row, with a normalise.sed beside them explaining why and saying what to delete when #841 is fixed. A golden records what the app does; the issue records what it should do.

Also fixed here, being in the way

  • run.sh never built menudump, so docker build failed on a fresh checkout — fixed by #11, now merged, which this branch is rebased on.
  • README.md embedded docs/images/macos/tray-menu.png captioned "The tray menu": a macOS-dressed Linux capture presented as the thing itself, which is policy point 5 broken in the most visible place in the repo. It now shows an undressed Linux capture, says so, and links the manual.
  • Captures move from docs/images/<theme>/tray-*.png to docs/images/<theme>/<scene>/. PR #9 also touches these paths (and commits a 4.4MB hack/screenshot/menudump binary by accident — worth dropping before it merges); whichever of us lands second re-runs ./run.sh --bless.

Dressing, still labelled as dressing

The suite captures undressed Linux and that is what the manual illustrates. One scene is additionally captured dressed for macOS and Windows, shown together at the end of the manual under one plain caveat. check.sh diffs the dressed dumps against the same goldens, which turns the caveat into an assertion: the dressing provably changes nothing about the menu. Genuine captures still need #648.

Verified

  • hack/screenshot/run.shtwo full runs, the second with nothing blessed, both green. All nine scenes plus the two dressed reproduce their goldens exactly.
  • hack/lint.sh — 0 issues. hack/coverage.sh — green, floors bumped (cmd/menudump 0.0 → 48.2).
  • manual.py check — green. actionlint — clean.
  • Run on arm64 (a Mac); CI will be the first amd64 run.

Evidence

Every capture in this PR is committed to the repo and quoted by docs/manual.md — the manual is the evidence, and it is version-controlled rather than pasted into a comment. The two that show most of what changed:

The tree, undressed, with the row order the first capture ever found wrong:

the menu

An unreachable platform, and #841's raw transport error making the menu 946px wide:

sync failed

Closes nothing automatically — this is the tray half of the platform repo's [#627](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/627), whose other half is a pin bump in that repo. A cross-repo `Closes` will not fire, so #627 gets closed by hand once both land. Rebased on merged #11 and #6; #6's VM-capture plan in `hack/screenshot/README.md` is kept in full, with the dressing paragraph reconciled by hand. `hack/screenshot` had one scene, one walk and one golden. It now has **nine scenes — one per state a user actually meets** — each asserted as text and each illustrated in a manual that is generated from the same run. ## What is here | | | |---|---| | **A scene per state** | `hack/screenshot/scenes/<name>/scene.json`: what the platform answers, and the walk that photographs it. Merged over `scenes/defaults.json`, so a scene says only what makes it that scene | | **Scenes that move through time** | `phases` replaces the listing N seconds in, which is the only way to capture a state that exists as a *change* — an application vanishing between two polls | | **One golden per scene** | every dump that scene's walk took, in one file, diffed by `check.sh`. Blessing is `--bless` and the diff is the review | | **A generated manual** | `docs/manual.md`, assembled by `manual.py` out of the scenes and what they captured. CI fails a manual that is not what a rebuild produces, so it cannot drift | | **A capture job in CI** | takes the captures *for real* and fails if they differ from the commit | The nine, in the order the manual meets them: signed out · nothing deployed · the tree and its gestures · an activation followed to its outcome · an activation refused for credit · a deployment that can do neither · an application that went away · one that turned up · the platform not answering. ## Three things about the harness that had to change **The walk names rows now; it does not count keypresses.** `menudump --to 'shop · #12|main · running'` reads the exported tree, skips exactly what GTK's keyboard skips, and prints the keystrokes that reach that row — so a row that moved is still reached, a row that is *gone* fails the run naming the labels it did find, and an ambiguous name fails rather than photographing whichever matched first. This is not tidying: **the counted walk had been wrong for two commits.** `docs/images/*/tray-activate.png` on `main` is a photograph of `Open in browser`, and `tray-gestures.png` is billing's `Activate` rather than the confirming stop it claims — both because the organization became a heading and every walk below it went one row too far. One of them was embedded in the README. **The shutter waits for the screen to stop moving.** Two identical consecutive frames, then keep that one. A third-level submenu under a compositor takes an unpredictable moment to paint and the difference between a good capture and a photograph of a half-drawn menu was about half a second, in either direction depending on the scene. **Some rows are clicked over the bus.** `menudump --click` sends dbusmenu's own `Event` — the same call a real desktop's host makes. The scenes that are *about* a toggle's consequences use it, because the toggles live one level deeper than this host draws reliably (see #844 below), and what those scenes photograph is the consequence anyway. `cmd/menudump` grew tests for all of that: the label→keystroke arithmetic is exactly the kind of logic whose bugs are silent, and it took the package from 0% to 48.2% (floor bumped). ## What the captures found — read, not just attached Policy point 2, doing its job. Five issues filed in the platform repo, none fixed here: | | | |---|---| | [#838](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/838) | Signed out, `sc-tray` says so only on a stderr nobody is reading — it refuses to start, and the user-visible state is an empty panel | | [#839](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/839) | The organization row's braille sparkline renders as a **dotted leader**, not a chart — legible as text, punctuation on screen | | [#841](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/841) | **The status row loses every sentence it is given**, to the resync the toggle itself triggers. The `N new application(s) — restart sc-tray` branch is unreachable dead code, overwritten two statements later in the same function | | [#844](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/844) | **The third level of the menu draws unreliably, so the deployment toggles sometimes cannot be reached at all.** The tray exports the rows correctly — `menudump` proves it — and the host draws an empty strip | | [#847](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/847) | A vanished application keeps **live toggles for deployments that no longer exist**: `update` retitles the row and `continue`s past the code that would have hidden them | #844 is the one worth a reviewer's attention. It is unresolved whether it is the app or the harness's host — the honest answer needs a second host (KDE's, or an XFCE panel) and I have not run one. Until it is answered the suite works around it *visibly* rather than quietly: - no `goto` may go deeper than two levels, and `scene.py` refuses one that tries, naming the issue; - the gesture rows are asserted **as text** in every golden — label, order, and which of the pair is hidden; - the `inert` scene deliberately has **no** photograph of its empty submenu, because such a picture is indistinguishable from #844; - the manual says all of this where a reader meets it. Two goldens (`in-flight`, `refused`) record the *wrong-but-current* status row, with a `normalise.sed` beside them explaining why and saying what to delete when #841 is fixed. A golden records what the app does; the issue records what it should do. ## Also fixed here, being in the way - `run.sh` never built `menudump`, so `docker build` failed on a fresh checkout — fixed by #11, now merged, which this branch is rebased on. - `README.md` embedded `docs/images/macos/tray-menu.png` captioned "The tray menu": a macOS-*dressed* Linux capture presented as the thing itself, which is policy point 5 broken in the most visible place in the repo. It now shows an undressed Linux capture, says so, and links the manual. - Captures move from `docs/images/<theme>/tray-*.png` to `docs/images/<theme>/<scene>/`. **PR #9 also touches these paths** (and commits a 4.4MB `hack/screenshot/menudump` binary by accident — worth dropping before it merges); whichever of us lands second re-runs `./run.sh --bless`. ## Dressing, still labelled as dressing The suite captures undressed Linux and that is what the manual illustrates. One scene is *additionally* captured dressed for macOS and Windows, shown together at the end of the manual under one plain caveat. `check.sh` diffs the dressed dumps against the same goldens, which turns the caveat into an assertion: the dressing provably changes nothing about the menu. Genuine captures still need [#648](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/648). ## Verified - `hack/screenshot/run.sh` — **two full runs, the second with nothing blessed, both green.** All nine scenes plus the two dressed reproduce their goldens exactly. - `hack/lint.sh` — 0 issues. `hack/coverage.sh` — green, floors bumped (`cmd/menudump` 0.0 → 48.2). - `manual.py check` — green. `actionlint` — clean. - Run on arm64 (a Mac); CI will be the first amd64 run. ## Evidence Every capture in this PR is committed to the repo and quoted by [docs/manual.md](docs/manual.md) — the manual *is* the evidence, and it is version-controlled rather than pasted into a comment. The two that show most of what changed: The tree, undressed, with the row order the first capture ever found wrong: ![the menu](https://git.grey.ooo/Grey.ooo/someones.computer_tray/raw/commit/8ab032284c027bb7ed49f63c5593157db869b71e/docs/images/linux/applications/menu.png) An unreachable platform, and #841's raw transport error making the menu 946px wide: ![sync failed](https://git.grey.ooo/Grey.ooo/someones.computer_tray/raw/commit/8ab032284c027bb7ed49f63c5593157db869b71e/docs/images/linux/unreachable/sync-failed.png)
Make the captures a scene per state, and generate the manual from them
Some checks failed
CI / build (pull_request) Successful in 2m14s
CI / captures (pull_request) Failing after 37s
8ab032284c
hack/screenshot had one scene, one walk and one golden. It now has nine
scenes — one per state a user actually meets — each asserted as text, and
each illustrated in docs/manual.md, which is assembled from the same run
and so cannot drift from it.

The scenes: signed out, nothing deployed, the tree and its gestures, an
activation followed to its outcome, one refused for credit, a deployment
that can do neither, an application that went away, one that turned up,
and the platform not answering. Several are whole-process states that one
container cannot share, and several exist only as a change over time, so
a scene owns its own run and `phases` lets it change its answer partway
through while the walk waits out the tray's poll.

Three things about the harness had to change to make that honest:

  * The walk names rows rather than counting keypresses. menudump --to
    reads the exported tree, skips exactly what GTK's keyboard skips, and
    prints the keystrokes that reach a named row — so a row that moved is
    still reached, and one that is gone fails the run naming what it did
    find. This is not tidying: the counted walk had been wrong for two
    commits, and docs/images/*/tray-activate.png was a photograph of
    "Open in browser".

  * The shutter waits for two identical consecutive frames. A third-level
    submenu under a compositor takes an unpredictable moment to paint, and
    a fixed sleep either wastes time on every shot or quietly photographs
    a half-drawn menu.

  * Rows a scene needs pressed are clicked over dbusmenu's own Event, the
    same call a real host makes, because the toggles sit one level deeper
    than this host draws reliably.

cmd/menudump grew tests for the label-to-keystroke arithmetic, whose bugs
are silent by nature; its floor rises from 0.0 to 48.2.

CI gains a job that takes the captures for real and fails if they differ
from the commit. check.sh on its own diffs two committed files, which is
how a golden reached main asserting a row main did not draw (#11).

What reading the captures found is filed rather than fixed: platform #838,
is worked around visibly rather than quietly: no goto may go deeper than
two levels, scene.py refuses one that tries, the gestures are asserted as
text instead, and the inert scene deliberately has no photograph of its
empty submenu, because such a picture cannot be told apart from the bug.

Two goldens record a wrong-but-current status row, with a normalise.sed
beside them explaining why and naming what to delete when #841 is fixed.

Also: README.md embedded a macOS-dressed Linux capture captioned "The tray
menu", which is policy point 5 broken in the most visible place in the
repo. It now shows an undressed Linux capture and says so.

Refs: Grey.ooo/Someones.Computer#627
Build the capture image into the daemon, not into the build cache
Some checks failed
CI / build (pull_request) Successful in 3m14s
CI / captures (pull_request) Failing after 7m55s
7a74d2887a
The captures job could not run a single scene. `docker build -t sc-tray-shot`
says the tag exists; on the house runner it does not. buildx's default
builder there is the `docker-container` driver, which leaves the result in
the build cache unless told otherwise — it warns about it, and the warning
scrolls past — so `docker run sc-tray-shot` a line later went looking for a
registry:

    docker: Error response from daemon: pull access denied for sc-tray-shot

`--load` gives the daemon the tag. It is harmless where the builder is
already the daemon's own, which is why this passed on a laptop.

Two things found alongside it:

  * The tag and the container names now carry a per-run discriminator. That
    daemon is the runner host's, shared with every other job and worktree
    on it, and a fixed `sc-tray-shot` is a name two concurrent runs would
    clobber. CI passes the run id.

  * The "committed captures describe this binary" step is gone. It diffed
    the captured menu dumps, and every dump carries the wall-clock time of
    its sync — so the raw files differ on every run by construction and the
    step could only ever have been red. The assertion it was reaching for
    already exists one line up: run.sh ends by running check.sh against
    what it just captured, with the time normalised, which is exactly
    "the goldens still describe the binary".
Pin upload-artifact to v3, which is the version this Forgejo implements
All checks were successful
CI / build (pull_request) Successful in 2m9s
CI / captures (pull_request) Successful in 7m1s
299ce78c8f
The captures job ran the whole suite green — eleven captures, every golden
as recorded — and then failed on the last step, uploading the pictures:

    :⚠️:Artifact upload failed with error: GHESNotSupportedError:
    @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
    are not currently supported on GHES

Forgejo identifies as GHES and does not implement the v2 artifact API that
upload-artifact@v4 requires, so a seven-minute run that proved the menu was
reported as a red job because a convenience upload at the end of it could
not work. The platform repo already pins v3 at every call site for exactly
this reason; this repo now does the same, documented at the call site.

Adds .github/actionlint.yaml with the matching suppression, since actionlint
judges action versions against GitHub's hosted runners and will call a
deliberate v3 pin too old. Same file and same reasoning as the platform
repo's.
geusebio changed title from WIP: A scene per state, asserted, and the manual generated from it to A scene per state, asserted, and the manual generated from it 2026-08-17 16:21:23 +00:00
Merge main, and re-capture every scene against the dashboard row
All checks were successful
CI / build (pull_request) Successful in 1m20s
CI / captures (pull_request) Successful in 8m18s
4e4e90993e
#9 landed while this branch was in CI. It touches the capture paths this
branch restructured and, more to the point, it changes the menu: there is an
`Open dashboard` row above the tree now, and links are opened through a
one-shot signed-in hand-off.

Resolved by keeping the per-scene structure. The old flat captures, stub.py
and the single menu.golden stay deleted — they are superseded by
docs/images/<theme>/<scene>/ and scenes/<name>/menu.golden — and where
rename detection paired one of them with a new per-scene file, the
per-scene file wins. coverage.floor takes both sides: cmd/menudump at 48.2
from here, internal/tray at 29.9 from there.

scene.py learns /cli/auth/browser-session, which #9's stub.py grew for the
same reason: nothing in the suite clicks a link, but the route should not be
a 404 if something did.

Then re-blessed, because #9's row makes every golden legitimately different.
The diff is one added line per dump, in the same place each time, and
nothing else moved — which is the suite doing its job: a menu change fails
the check until somebody looks at it.

One prose change, in the unreachable scene: `Open dashboard` keeps working
with nothing answering, because the signed-in hand-off falls back to the
plain address. Worth saying, since the row's whole point is the hand-off.
Sign in to join this conversation.
No reviewers
No labels
in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/someones.computer_tray!12
No description provided.