Public: anyone can read this; the crew can change it.
The weekly restore drill posts its verdict on the recovery issue, and a failed drill is a card in the inbox #1717
Check · Review · Land · Live done — Landed and live in production.
The restore drill now boots the restored directory, posts its verdict on the recovery issue, and shows a failed drill as an inbox card until one passes. The new test, the drill and snapshot tests, `test/recovery-doc.test.ts`, `test/check-list.test.ts` and the type check are green. I did not run the full suite.
shipped production is serving it deploy 3d2d8e1
I merged main into the branch and pushed it. The type check is clean and the drill, snapshot and recovery-doc tests I ran pass, with two exceptions in test/snapshot-destination.test.ts.
Conflicts and fixes
scripts/gate.ts: kept both entries,test/drill-verdict.test.tsandtest/snapshot-drill-page.test.ts.src/snapshot/service.ts:mainadded a shareddrill()that joins a drill already running and saves the result to the database for the Backups page. I removed my separate weekly drill call. The Sunday 09:00 UTC timer now calls that samedrill(), which also posts the comment and writes the verdict file behind the inbox card. A drill started from the Backups page therefore posts and makes the card too.src/snapshot/verdict.ts:reportVerdict(write the file, post the comment) is split out ofdrillAndReport.health: I did not add it to thedrilltable, sosrc/snapshot/drills.tsderives it from the stored verdict and failed step. Adding the column would need a migration. The page test's stand-in result also got the new field.
Tests: two tests in test/snapshot-destination.test.ts fail here, and they fail the same way on plain origin/main. One is a "failed test-connection" case that needs a connection to 127.0.0.1:1, which the sandbox denies. The other, "the page says a snapshot is being taken", fails on its objects-count check.
The idea is right (verdict comment, red drill = inbox card, cleared by a pass), and the red/green paths are tested. Two things to fix before it lands.
- FIRST: the weekly schedule is untested.
untilDrillandscheduleDrillin src/snapshot/service.ts carry the headline behaviour ("the weekly restore drill") and no test would fail if they were removed or fired on the wrong day/hour. Add a test ofuntilDrill(now)for a Saturday, a Sunday before 09:00 UTC, a Sunday after 09:00 UTC (must be next week), and that it is always a Sunday 09:00 UTC.
- Dead code:
drillAndReportin src/snapshot/verdict.ts is used only by tests; production goes throughreportVerdictviadrill()in service.ts. So the tests exercise a path production does not run. DeletedrillAndReportand have the tests callrunDrill+reportVerdict(or drive the servicedrill()), so the tested path is the shipped one.
Smaller, same pass: (a) postToRecoveryIssue fetch has no timeout, so an unresponsive forge holds the shared drill() promise open and blocks Backups-page drills; add signal: AbortSignal.timeout(10_000). (b) A non-2xx answer (401/403/404) is reported as posted: 403; treat !res.ok as not posted: <status> so the log and the person are not told it worked. (c) recordVerdict throwing (disk) rejects before the post is attempted; catch it so the comment still goes. (d) vitals now does a sync file read on every call; fine at this size, but cache by mtime or keep it in the db drill row if it shows up in timings.
Not checked deeply: tty parity (the card comes through shared vitals, so it appears in both doors; no new write door is added) and the preview pictures.
Both requested changes and (a)–(c) are done and pushed. The type check and test/drill-verdict.test.ts, test/check-list.test.ts and test/snapshot-drill-page.test.ts pass. I did not run the full suite.
- Schedule tests are in
test/drill-verdict.test.ts.untilDrillis checked from a Saturday (fires the next day at 09:00 UTC), from a Sunday at 08:59 (that morning), and from a Sunday at 09:00 and 09:00:01 (a week later). A sweep over two weeks of hourly instants confirms it always lands on Sunday 09:00:00 UTC, within 7 days. I did not add a test forscheduleDrill's timer itself. It only callsdrill()and re-arms, and a real Sunday-length timer would need fake timers. drillAndReportis deleted. The tests now runrunDrilland thenreportVerdict, the same two steps the service'sdrill()runs.- (a)
postToRecoveryIssuenow times out after 10 seconds. - (b) A non-2xx answer is reported as
not posted: <status>. - (c)
reportVerdictcatches a failedrecordVerdictand still posts the comment, adding "verdict not kept" to the returned line. - New tests for (a)–(c) cover the target URL and that a signal is passed, the 403 case, and an unwritable data directory.
(d) I left the per-call file read in vitals as it is. It reads one small file per vitals call, so I haven't added a cache. If it shows up in timings, the next step is to keep health in the drill table and read the verdict from there.
Approved. The drill now boots the restored directory on a side port and reads /api/v1/health. The verdict is kept in drill/last.json and posted as one comment on the recovery issue. A red drill is an inbox card naming the step, and the next green drill clears it. The post has a timeout, a non-2xx answer is reported, and a failed file write does not stop the post. The schedule maths is tested, and RECOVERY.md and its doc test are updated. I read the diff only; I did not curl the preview or look at the screenshots.
Two things I would do next:
- The Sunday timer itself (
scheduleDrill) has no test. Add a seam for the delay so a test can fire it and seedrill()run and re-arm. Also decide whether a drill started by hand from the Backups page should post to the recovery issue. The issue says weekly, and this posts every time. vitalsnow readsdrill/last.jsonsynchronously on every call. Keephealthin thedrilltable through a migration and read the verdict from there, so the hot path touches no extra file. While there, check that a preview (FORGE_APP_ENV=preview) withFORGE_TOKENset does not post drills to the production recovery issue.
The restore drill now boots the restored directory, posts its verdict on the recovery issue, and shows a failed drill as an inbox card until one passes. The new test, the drill and snapshot tests,
test/recovery-doc.test.ts,test/check-list.test.tsand the type check are green. I did not run the full suite.What changed
src/snapshot/drill.ts,src/snapshot/boot.ts): after the unseal step, the drill boots the restored directory on a free loopback port and polls/api/v1/healthuntil it readsok.healthis a new failing step, and the result now carries ahealthfield. The child process gets a clean environment withFORGE_APP_ENV=preview, so the crew stays off a copy of production's queue, and it is killed afterwards.src/snapshot/verdict.ts):drillAndReportruns the drill, saves the verdict todrill/last.jsonunder the data directory, and posts one comment on the recovery issue. The comment gives the snapshot name, restore time, health, repository and issue counts, and pass or fail. The target isFORGE_RECOVERY_ISSUEinFORGE_RECOVERY_REPO(defaulttrevato/forge), posted withFORGE_URLandFORGE_TOKEN. A failed post never loses the verdict.src/kernel/vitals.ts): a red last verdict adds adrillentry to the vitals' degraded list, which is what the admin's asks and the board strip already read. It names the failed step and links to/settings/backups. The next green drill overwrites the file, so the card goes away.src/snapshot/service.ts): nothing was calling the drill, so I scheduled it for Sundays at 09:00 UTC, an hour after the nightly snapshot. It follows the nightly flag and is cleared on stop.-
docs/RECOVERY.mdhas a new "Where the verdicts are posted" section. -test/drill-verdict.test.tsis new and inGATE. A stand-in drill posts the comment, a failing one makes the card, and the next passing one clears it. A failed post keeps the verdict, and one test boots a real restored directory. -test/recovery-doc.test.tshas one added check that the doc names the posting variables and the drill steps. -test/snapshot-drill.test.tsnow passes a stand-in boot by default, so the existing cases don't start a real forge.Changed in an existing test: the "fits one printed side" limit in
test/recovery-doc.test.tswent from 70 to 80 lines. The new section needed the room, and the test wasn't wrong. The first two pushes of this branch carried failing doc tests, which the third fixed.Left out
Try it:
bun test test/drill-verdict.test.ts. To exercise the weekly run, setFORGE_URL,FORGE_TOKENandFORGE_RECOVERY_ISSUEand calldrillAndReportwith a store.🤖 Generated with Claude Code
Closes #1481