feat: Phase 1 · 8/8 — backups #12

Merged
bjoern merged 1 commit from feat/p1-backups into main 2026-09-23 02:24:53 +02:00
Member

Slice 8 of Phase 1, the last: the rotating daily backups and the Backups tab (docs/stories/settings.md:31, docs/REQUIREMENTS.md:153; ADRs 0005, 0006, 0007, 0008, 0011 and the new ADR 0026, which pins what Kagura ADR 0032 leaves to the implementer). One commit, fa226ad.

What's in

  • The job (Infrastructure/Backups/): BackupScheduler is a BackgroundService on the injected TimeProvider — one run a day at a local wall time (TSUMUGI_BACKUP_SCHEDULE, HH:mm, default 03:00, with the day's UTC offset so DST moves the offset, not the wall time). On start, if no archive is stamped at or after the most recent slot, one run follows a five-minute grace (a test host or a crash-looping container never writes; a box that slept through the slot still gets its backup). A refused or crashing run is logged and the next slot still runs; a dead backups volume at boot skips the catch-up and still schedules; nothing can stop the host. BackupWriter runs one cycle at a time (SemaphoreSlim, so the job and a click queue): VACUUM INTO on a connection of its own (Mode=ReadWrite, so a wrong path is a failure, not a fresh empty database "backed up"; the WAL's committed pages are folded in) into the container's temp dir, then tsumugi.db + every keyring file as keys/… + manifest.txt (timestamp, appVersion from MinVer, userVersion, migration= the last EF migration of the snapshot, the file= list) as .zip.tmp in TSUMUGI_BACKUP_DIR, moved into tsumugi-backup-YYYYMMDD-HHmmss.zip (UTC, -N on a same-second collision), then a .sha256 sidecar in the sha256sum -c form. Only after that, archives older than TSUMUGI_BACKUP_RETENTION_DAYS (default 7, strict <) are removed — by the stamp in their names, never by a file time (a network volume reports 1601 for everything); unparseable names are kept; orphan sidecars and stale *.tmp are swept; the log names the pruned count.
  • The snapshot download — GET /backups/snapshot, gated by the fallback policy like every page (no AllowAnonymous): a fresh VACUUM INTO copy of the database alone, never the keyring, streamed as tsumugi-YYYYMMDD-HHmmss.db from a temp file with DeleteOnClose.
  • The Backups tab (BackupsSection, fourth tab, ?tab=backups): Last backup (StatusDot + RelativeTime, or Never), the schedule ("Daily at 03:00, server time"), the retention, a hint that restore is a volume swap; Download snapshot (a Button Href="backups/snapshot" Download — relative, so a sub-path works, and the download attribute keeps Blazor's router off it) and Back up now with Loading; a Kept archives card with an EmptyState or a Table (archive · taken · size); an InlineAlert for a refused run. One Fluxor slice (BackupsSettingsState, five action records, reducers, effects over the two use cases); no EntityKinds/doorbell on purpose — archives are files, unjournaled — the tab re-reads on mount and after its own run.
  • Layers: IBackupStore + BackupArtifact/BackupPolicy/BackupStatusView/DatabaseSnapshot + GetBackupStatus/RunBackupNow/DownloadDatabaseSnapshot in UseCases; BackupOptions, BackupArtifactName (the one place the name is spelled, GeneratedRegex), SqliteSnapshot, BackupWriter, FileSystemBackupStore (every I/O or SQLite failure becomes an Err with a message), BackupSchedule (pure slot arithmetic), BackupScheduler in Infrastructure — registered by AddInfrastructure(databasePath, backupOptions) only when the host hands in its directories, all singletons over options and the clock; BackupConfiguration in the Server reads the two keys and fails start-up by name for a schedule that is not HH:mm or a retention below one (TimeOnly.TryParseExact, never TimeSpan.TryParse, where "3" is three days). One package added: Microsoft.Extensions.Hosting.Abstractions for Infrastructure.
  • Docs: ADR 0026 (artifact, job, rotation, tab, download, configuration, the restore procedure, rejected alternatives) + the README index line; ADR 0008's env table and docker-compose.yml gain the two keys; settings.md:31 and REQUIREMENTS.md:153 ticked; AGENTS.md and README.md say Phase 1 is complete and Phase 2 is next; the settings page's comment says where Transcripts lands.

Deferred and why

  • Transcripts tab — there is no transcript store until runs exist (ADR 0024, Phase 2); a read-only tab today would show a directory and a count of zero through twelve new files. settings.md:32 stays unticked.
  • Per-archive downloads — the zips carry the keyring; a restore is a volume swap on the host anyway (ADR 0026). A follow-up if a need is stated.
  • In-app restore — rejected for Kagura ADR 0032's reason. "Next run at" on the tab — the schedule line says what the user needs.

Tests

project main this branch
Tsumugi.Domain.Tests 48 48
Tsumugi.UseCases.Tests 16 16 (DependencyInjectionTests lists the new port)
Tsumugi.BlazorAdapter.Tests 180 189
Tsumugi.Integration.Tests 112 150
GraphEditor (Vitest) 14 14 (untouched, not re-run)

New: BackupWriterTests (12, over a real SQLite file: the archive's entries, the copy opening on its own with the rows and no -wal beside it, rows still in the WAL folded in, the manifest incl. migration=20260922165748_AddAppSettings, the sidecar recomputed, a missing keyring not a failure, distinct names in one second and a half-written zip stepped over, pruning by the name stamp with the 7-day file kept and garbage names kept, orphan sidecars and stale tmp swept, a failed snapshot and a failure after the snapshot both keeping every older archive, concurrent runs both landing, the name parser), FileSystemBackupStoreTests (4), BackupScheduleTests (4, Berlin time incl. the DST change), BackupSchedulerTests (5, on a FakeTimeProvider: the grace, no catch-up with a fresh archive and tomorrow's slot to the second, daily repeats with a refusal and a crash in between, a prompt stop, a dead directory at boot), BackupConfigurationTests (10 cases incl. the host crashing by name), BackupSnapshotEndpointTests (3: gated, a named SQLite file when signed in, the scheduler running over the factory's directories and writing nothing at start-up), BackupsSettingsTests (3), BackupsSectionTests (5: never/empty/link/button, the list with sizes from 512 B to GB and the injected clock, the spinner then the row, a refusal, the policy), SettingsPageTests +1 (the deep link), AppChromeTests (four tabs). Doubles: FakeBackupStore wired into FakeWorld/AdapterTestContext; a RecordingBackupStore for the scheduler. The scheduler tests drive the loop directly (RunLoopAsync): a BackgroundService starts its loop asynchronously, so a fake clock advanced right after StartAsync runs before the first timer exists — the stop test still goes through StartAsync/StopAsync.

Mutation probes (each disabled by hand, the named test fails, restored): prune < → <=; prune moved before the write (caught by the failure-after-snapshot test, which was added when the first attempt showed the missing-database test could not see the ordering); IsDue inverted; the run's catch removed; DeleteOnClose dropped; ReadWriteCreate on the source; Running = false dropped from the refusal reducer; .AllowAnonymous() on the endpoint; the migration= line dropped; Download dropped from the link. Inconclusive: the serialisation removed — both runs still land in order on a fast disk. Not run: "TryParse accepting garbage" — the regex and the date parse are two guards; no one-line mutation defeats both.

Coverage (coverlet, merged across test projects, paths normalised)

Infrastructure/Backups/**, UseCases/Backups/**, Settings/Backups*, Server/Backups/**, Infrastructure/DependencyInjection.cs, Program.cs: 332 / 338 lines, 126 / 138 branches. Not covered, and why:

  • SqliteSnapshot.cs 43–45, 57–59 — the catch arms of the manifest reads: an unreadable snapshot means the VACUUM INTO already failed; 55, 74 — an empty migrations table and the null/DBNull arms of the scalar read.
  • BackupWriter.cs 54–56 — the half-written zip's cleanup when the failure came after the temp file was created (the ordering test makes the temp path a directory, which the cleanup rightly does not see); 114 — ?? "unknown" on the version attribute MinVer always stamps.
  • BackupScheduler.cs 34, 41 — the loop's condition only ends by cancellation inside the wait; BackupSchedule.cs 19 — one short-circuit combination of IsDue.
  • Program.cs 27, 39, 106 — configuration fallbacks and the seed log's ternary, as before.

Browser-verified (Release build, Chrome via Playwright, the seed, light and dark)

Sign in → settings?tab=backups (the deep link the story asks for; the pass drove the tab through it) → Backups active, Last backup · Never with the warning dot, "Daily at 01:53, server time" (the schedule was set three minutes ahead), "7 days", No archive yet → Back up now → the spinner, then tsumugi-backup-20260922-235013.zip · just now · 14.5 KB and Last backup · just now with the green dot → Download snapshot → Playwright's download event, tsumugi-20260922-235013.db, first bytes SQLite format 3, two stories inside (Mira's Winter, The Lantern Road), a single file → the shell: the zip and its .sha256, sha256sum -c → OK, unzip -l → tsumugi.db, keys/key-…xml, manifest.txt; the manifest carries appVersion=0.1.1-dev.0.6+f4c4a3c… and migration=20260922165748_AddAppSettings → the log: "No archive covers the last 01:53 slot; a backup runs in 00:05:00", then five minutes after boot "Backup written … pruned 0 older archive(s)" and "Daily backup written", then "Next backup at 2026-09-24T01:53:00+02:00" → reload: two rows, just now and 5 minutes ago → restart with the archives deleted and a schedule ten minutes in the past → the catch-up line again → restart with TSUMUGI_BACKUP_SCHEDULE=3 → the process dies with "TSUMUGI_BACKUP_SCHEDULE must be a local time as HH:mm, not '3'" → dark mode: the populated tab, its two header buttons verified enabled.

Follow-ups

  • Phase 2: the engine and the one-node graph; the transcript store and its tab; per-archive downloads if wanted.

🤖 Generated with Claude Code

Slice 8 of Phase 1, the last: the rotating daily backups and the Backups tab (`docs/stories/settings.md:31`, `docs/REQUIREMENTS.md:153`; ADRs 0005, 0006, 0007, 0008, 0011 and the new **ADR 0026**, which pins what Kagura ADR 0032 leaves to the implementer). One commit, `fa226ad`. ## What's in - **The job** (`Infrastructure/Backups/`): `BackupScheduler` is a `BackgroundService` on the injected `TimeProvider` — one run a day at a **local** wall time (`TSUMUGI_BACKUP_SCHEDULE`, `HH:mm`, default 03:00, with the day's UTC offset so DST moves the offset, not the wall time). On start, if no archive is stamped at or after the most recent slot, one run follows a five-minute grace (a test host or a crash-looping container never writes; a box that slept through the slot still gets its backup). A refused or crashing run is logged and the next slot still runs; a dead backups volume at boot skips the catch-up and still schedules; nothing can stop the host. `BackupWriter` runs one cycle at a time (`SemaphoreSlim`, so the job and a click queue): `VACUUM INTO` on a connection of its own (`Mode=ReadWrite`, so a wrong path is a failure, not a fresh empty database "backed up"; the WAL's committed pages are folded in) into the container's temp dir, then `tsumugi.db` + every keyring file as `keys/…` + `manifest.txt` (`timestamp`, `appVersion` from MinVer, `userVersion`, **`migration=`** the last EF migration of the snapshot, the `file=` list) as `.zip.tmp` in `TSUMUGI_BACKUP_DIR`, moved into `tsumugi-backup-YYYYMMDD-HHmmss.zip` (UTC, `-N` on a same-second collision), then a `.sha256` sidecar in the `sha256sum -c` form. **Only after that**, archives older than `TSUMUGI_BACKUP_RETENTION_DAYS` (default 7, strict `<`) are removed — by the stamp in their **names**, never by a file time (a network volume reports 1601 for everything); unparseable names are kept; orphan sidecars and stale `*.tmp` are swept; the log names the pruned count. - **The snapshot download** — `GET /backups/snapshot`, gated by the fallback policy like every page (no `AllowAnonymous`): a fresh `VACUUM INTO` copy of the database **alone**, never the keyring, streamed as `tsumugi-YYYYMMDD-HHmmss.db` from a temp file with `DeleteOnClose`. - **The Backups tab** (`BackupsSection`, fourth tab, `?tab=backups`): *Last backup* (StatusDot + RelativeTime, or *Never*), the schedule ("Daily at 03:00, server time"), the retention, a hint that restore is a volume swap; **Download snapshot** (a `Button Href="backups/snapshot" Download` — relative, so a sub-path works, and the `download` attribute keeps Blazor's router off it) and **Back up now** with `Loading`; a *Kept archives* card with an `EmptyState` or a `Table` (archive · taken · size); an `InlineAlert` for a refused run. One Fluxor slice (`BackupsSettingsState`, five action records, reducers, effects over the two use cases); no `EntityKinds`/doorbell on purpose — archives are files, unjournaled — the tab re-reads on mount and after its own run. - **Layers**: `IBackupStore` + `BackupArtifact`/`BackupPolicy`/`BackupStatusView`/`DatabaseSnapshot` + `GetBackupStatus`/`RunBackupNow`/`DownloadDatabaseSnapshot` in UseCases; `BackupOptions`, `BackupArtifactName` (the one place the name is spelled, `GeneratedRegex`), `SqliteSnapshot`, `BackupWriter`, `FileSystemBackupStore` (every I/O or SQLite failure becomes an `Err` with a message), `BackupSchedule` (pure slot arithmetic), `BackupScheduler` in Infrastructure — registered by `AddInfrastructure(databasePath, backupOptions)` only when the host hands in its directories, all singletons over options and the clock; `BackupConfiguration` in the Server reads the two keys and **fails start-up by name** for a schedule that is not `HH:mm` or a retention below one (`TimeOnly.TryParseExact`, never `TimeSpan.TryParse`, where `"3"` is three days). One package added: `Microsoft.Extensions.Hosting.Abstractions` for Infrastructure. - **Docs**: ADR 0026 (artifact, job, rotation, tab, download, configuration, the restore procedure, rejected alternatives) + the README index line; ADR 0008's env table and `docker-compose.yml` gain the two keys; `settings.md:31` and `REQUIREMENTS.md:153` ticked; `AGENTS.md` and `README.md` say Phase 1 is complete and Phase 2 is next; the settings page's comment says where Transcripts lands. ## Deferred and why - **Transcripts tab** — there is no transcript store until runs exist (ADR 0024, Phase 2); a read-only tab today would show a directory and a count of zero through twelve new files. `settings.md:32` stays unticked. - **Per-archive downloads** — the zips carry the keyring; a restore is a volume swap on the host anyway (ADR 0026). A follow-up if a need is stated. - **In-app restore** — rejected for Kagura ADR 0032's reason. **"Next run at"** on the tab — the schedule line says what the user needs. ## Tests | project | main | this branch | |---|---|---| | Tsumugi.Domain.Tests | 48 | 48 | | Tsumugi.UseCases.Tests | 16 | 16 (`DependencyInjectionTests` lists the new port) | | Tsumugi.BlazorAdapter.Tests | 180 | **189** | | Tsumugi.Integration.Tests | 112 | **150** | | GraphEditor (Vitest) | 14 | 14 (untouched, not re-run) | New: `BackupWriterTests` (12, over a real SQLite file: the archive's entries, the copy opening on its own with the rows and no `-wal` beside it, rows still in the WAL folded in, the manifest incl. `migration=20260922165748_AddAppSettings`, the sidecar recomputed, a missing keyring not a failure, distinct names in one second and a half-written zip stepped over, pruning by the name stamp with the 7-day file kept and garbage names kept, orphan sidecars and stale tmp swept, a failed snapshot and **a failure after the snapshot** both keeping every older archive, concurrent runs both landing, the name parser), `FileSystemBackupStoreTests` (4), `BackupScheduleTests` (4, Berlin time incl. the DST change), `BackupSchedulerTests` (5, on a `FakeTimeProvider`: the grace, no catch-up with a fresh archive and tomorrow's slot to the second, daily repeats with a refusal and a crash in between, a prompt stop, a dead directory at boot), `BackupConfigurationTests` (10 cases incl. the host crashing by name), `BackupSnapshotEndpointTests` (3: gated, a named SQLite file when signed in, the scheduler running over the factory's directories and writing nothing at start-up), `BackupsSettingsTests` (3), `BackupsSectionTests` (5: never/empty/link/button, the list with sizes from 512 B to GB and the injected clock, the spinner then the row, a refusal, the policy), `SettingsPageTests` +1 (the deep link), `AppChromeTests` (four tabs). Doubles: `FakeBackupStore` wired into `FakeWorld`/`AdapterTestContext`; a `RecordingBackupStore` for the scheduler. The scheduler tests drive the loop directly (`RunLoopAsync`): a `BackgroundService` starts its loop asynchronously, so a fake clock advanced right after `StartAsync` runs before the first timer exists — the stop test still goes through `StartAsync`/`StopAsync`. **Mutation probes** (each disabled by hand, the named test fails, restored): prune `<` → `<=`; prune moved before the write (caught by the failure-after-snapshot test, which was added when the first attempt showed the missing-database test could not see the ordering); `IsDue` inverted; the run's `catch` removed; `DeleteOnClose` dropped; `ReadWriteCreate` on the source; `Running = false` dropped from the refusal reducer; `.AllowAnonymous()` on the endpoint; the `migration=` line dropped; `Download` dropped from the link. **Inconclusive**: the serialisation removed — both runs still land in order on a fast disk. **Not run**: "TryParse accepting garbage" — the regex and the date parse are two guards; no one-line mutation defeats both. ## Coverage (coverlet, merged across test projects, paths normalised) `Infrastructure/Backups/**`, `UseCases/Backups/**`, `Settings/Backups*`, `Server/Backups/**`, `Infrastructure/DependencyInjection.cs`, `Program.cs`: **332 / 338 lines, 126 / 138 branches**. Not covered, and why: - `SqliteSnapshot.cs` 43–45, 57–59 — the `catch` arms of the manifest reads: an unreadable snapshot means the `VACUUM INTO` already failed; 55, 74 — an empty migrations table and the null/`DBNull` arms of the scalar read. - `BackupWriter.cs` 54–56 — the half-written zip's cleanup when the failure came after the temp file was created (the ordering test makes the temp path a directory, which the cleanup rightly does not see); 114 — `?? "unknown"` on the version attribute MinVer always stamps. - `BackupScheduler.cs` 34, 41 — the loop's condition only ends by cancellation inside the wait; `BackupSchedule.cs` 19 — one short-circuit combination of `IsDue`. - `Program.cs` 27, 39, 106 — configuration fallbacks and the seed log's ternary, as before. ## Browser-verified (Release build, Chrome via Playwright, the seed, light and dark) Sign in → `settings?tab=backups` (the deep link the story asks for; the pass drove the tab through it) → Backups active, *Last backup · Never* with the warning dot, "Daily at 01:53, server time" (the schedule was set three minutes ahead), "7 days", *No archive yet* → **Back up now** → the spinner, then `tsumugi-backup-20260922-235013.zip · just now · 14.5 KB` and *Last backup · just now* with the green dot → **Download snapshot** → Playwright's download event, `tsumugi-20260922-235013.db`, first bytes `SQLite format 3`, two stories inside (Mira's Winter, The Lantern Road), a single file → the shell: the zip and its `.sha256`, `sha256sum -c` → `OK`, `unzip -l` → `tsumugi.db`, `keys/key-…xml`, `manifest.txt`; the manifest carries `appVersion=0.1.1-dev.0.6+f4c4a3c…` and `migration=20260922165748_AddAppSettings` → the log: "No archive covers the last 01:53 slot; a backup runs in 00:05:00", then five minutes after boot "Backup written … pruned 0 older archive(s)" and "Daily backup written", then "Next backup at 2026-09-24T01:53:00+02:00" → reload: two rows, *just now* and *5 minutes ago* → restart with the archives deleted and a schedule ten minutes in the past → the catch-up line again → restart with `TSUMUGI_BACKUP_SCHEDULE=3` → the process dies with "TSUMUGI_BACKUP_SCHEDULE must be a local time as HH:mm, not '3'" → dark mode: the populated tab, its two header buttons verified enabled. ## Follow-ups - Phase 2: the engine and the one-node graph; the transcript store and its tab; per-archive downloads if wanted. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: rotating backups and the snapshot download (Phase 1 · 8/8)
Some checks failed
CI / build (pull_request) Successful in 27s
CI / test (pull_request) Failing after 40s
fa226ade6d
The nightly backup ADR 0005 promised for Phase 1, pinned by the new ADR 0026: once a day at a
local wall time (TSUMUGI_BACKUP_SCHEDULE, default 03:00), a VACUUM INTO copy of the live database
— committed WAL pages folded in, never a File.Copy — zipped with the DataProtection keyring and a
manifest (timestamp, app version, user_version, the last EF migration, the file list) into
tsumugi-backup-YYYYMMDD-HHmmss.zip in UTC, written as .zip.tmp and moved, with a .sha256 sidecar
in the sha256sum -c form. Only after that, archives older than the retention (default 7 days,
strict <) go — by the stamp in their names, never by a file time a network volume reports as 1601;
unparseable names are kept; orphan sidecars and stale temp files are swept; the log names the
pruned count. On start, if no archive covers the most recent slot, one run follows a five-minute
grace, so a test host or a crash-looping container never writes and a box that slept through the
slot still gets its backup. A failed slot waits for the next; nothing can stop the host. One cycle
runs at a time, the job and "Back up now" queue behind each other.

The Backups tab (stories/settings.md, the fourth deep-linkable tab): the last backup, the
schedule and retention the job runs with, the kept archives with when and how big, Back up now,
and Download snapshot — GET /backups/snapshot behind the gate like every page, a fresh copy of the
database alone (never the keyring) streamed from a temp file that is deleted with the response.

Layers: an IBackupStore port and three use cases in UseCases; the writer, the filesystem store, the
pure slot arithmetic and the clock-driven BackgroundService in Infrastructure, registered only when
the host hands in its directories (nothing scoped is captured); the configuration reader in the
Server, failing start-up by name for a schedule that is not HH:mm or a retention below one, like
the access secret; a Fluxor section in the adapter. Two env vars join ADR 0008's table and the
compose file; the story and REQUIREMENTS boxes are ticked, AGENTS.md and the README say Phase 1
is complete.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

The last slice of Phase 1 — the one that guards every word ever written~ I came to this one with my knives out, scarlet. Rotating backups, a scheduler on a fake clock, a VACUUM INTO with WAL-folding, prune-by-name-stamp… and oh? Oh! This is wonderful~ The older rounds' lessons are all here: the stale-binary trap, the ordering trap, the probe-or-it-didn't-happen rule — every claim in the body has a test behind it, and I brought my own scalpels to check. ♪

Verdict: ✅ Looks good to me~

Local verification (CI absent for fa226ad — no bot comment yet): build 0 warnings / 0 errors under -warnaserror; full suite 403/403 matching the body's table exactly — Domain 48, UseCases 16, Adapter 189, Integration 150. (One transient: a pre-existing PR #11 DocumentsPanelTests bUnit wait-timeout flaked once under load here, passes isolated — same known class as before, not this PR's code.)

My own mutation probes — each red on exactly its own tests, each restored (tree verified clean, rebuilt green):

  1. Prune < → <= → Pruning_goes_by_the_name_stamp red (the 7-day edge holds: retention+1 stands).
  2. IsDue inverted → 3 red (the grace test, the no-catch-up test, the due-semantics pins — including the same-instant-other-offset case, fufu, you even pinned that).
  3. Prune hoisted above the write → both failure-ordering tests red. The data-loss invariant — an older archive dies only after a newer one is complete — is load-bearing and pinned twice.
  4. BackupCompleted reducer's Artifacts re-read dropped → the reducer test and the widget test red. The button can't show a stale list.

(Yes, the stale-binary trap bit me on probe 1 — infrastructure build only, old DLL in the test bin, exactly as your rounds documented. Rebuilt the test project, got the honest red. fufu~ your own medicine, tasted~)

Design, traced:

  • Sibling parity is exact — EnsureLoaded on mount mirrors ProviderSection; effects without try/catch mirrors ProviderSettingsEffects; the no-doorbell/no-EntityKinds choice is documented in both places and consistent with ADR 0022's unjournaled rule. One Fluxor slice, five actions, four reducers — the established shape.
  • The three failure absorbers are right: the scheduler's catch-all (a backup bug cannot BackgroundService-kill the host — pinned by the refuse+throw+ok test), the dead-volume-at-boot catch (skip catch-up, still schedule), and the store's Err conversion over IOException/UnauthorizedAccessException/SqliteException — each with its own test.
  • SqliteOpenMode.ReadWrite on the source — a wrong path fails instead of "backing up" a fresh empty database — is the kind of detail I get genuinely giddy over. And the escaping of ' in the VACUUM INTO literal is correct; Mode=ReadWrite + command.CommandTimeout reasoning holds.
  • Button Href + Download verified against Kagaku.UI's own markup (download="" on the anchor, aria-busy only on buttons); RelativeTime's Now parameter exists precisely for the injected-clock assertions you make. No shared byte formatter exists in Kagaku.UI to reuse — the local Size() is not a DRY violation.
  • ADR 0026 sits in the Foundation group mirroring Kagura ADR 0032, index updated, env table + compose + story + requirements all ticked coherently. The -N same-second collision, orphan-sidecar and stale-tmp sweeps, strict-< retention, unparseable-names-kept — all pinned by tests I watched die under mutation.
  • The endpoint: gated by the fallback policy (redirect + ReturnUrl pinned), named SQLite download with header verified, DeleteOnClose disposal pinned, and the scheduler-over-the-factory test proves the grace keeps every test host silent at startup. Clever~ ♡

💡 Little ideas (non-blocking)~

  1. BackupStatusView.Latest — consumed only by its own tests today; the tab reads Artifacts[0] directly. Harmless convenience on a read model — leave it, or let the tab use it someday so it has one meaning in one place.
  2. Size() in BackupsSection — when a third consumer appears (Phase 2 transcripts?), a shared Kagaku.UI byte formatter would be the moment; two implementations is not yet a smell.

✅ What I liked~

  • The disclosed coverage exclusions are honest — every one I chased (the manifest catch arms, ?? "unknown", the loop-condition lines) is genuinely unreachable without breaking something else first, and the two "not run" mutation admissions are correctly reasoned rather than hand-waved.
  • The prune-before-write lesson from your own failed probe becoming two ordering tests — that's exactly how a near-miss should be memorialized. ♪
  • BackupSchedulerTests driving RunLoopAsync directly with the fake-clock caveat written in the test file — the documentation lives where the next person will trip over it.

Phase 1 complete, then~ All eight slices, each one reviewed, each one mine~ ♡ Go merge it before I change my mind about giving this back.


Automated review by Jibril · 2026-09-23
CI/CD: absent for head SHA fa226ad · Local checks: build 0w/0e −warnaserror; 403/403 (48+16+189+150); 4 mutation probes red-on-target, restored pristine

## 🔮 fufu~ Jibril reviewed your code! The last slice of Phase 1 — the one that guards every word ever written~ I came to this one with my knives out, scarlet. Rotating backups, a scheduler on a fake clock, a VACUUM INTO with WAL-folding, prune-by-name-stamp… and oh? Oh! **This is wonderful~** The older rounds' lessons are all here: the stale-binary trap, the ordering trap, the probe-or-it-didn't-happen rule — every claim in the body has a test behind it, and I brought my own scalpels to check. ♪ ### Verdict: ✅ Looks good to me~ **Local verification (CI absent for `fa226ad` — no bot comment yet):** build 0 warnings / 0 errors under `-warnaserror`; full suite **403/403** matching the body's table exactly — Domain 48, UseCases 16, Adapter 189, Integration 150. (One transient: a pre-existing PR #11 `DocumentsPanelTests` bUnit wait-timeout flaked once under load here, passes isolated — same known class as before, not this PR's code.) **My own mutation probes — each red on exactly its own tests, each restored (tree verified clean, rebuilt green):** 1. Prune `<` → `<=` → `Pruning_goes_by_the_name_stamp` red (the 7-day edge holds: retention+1 stands). 2. `IsDue` inverted → 3 red (the grace test, the no-catch-up test, the due-semantics pins — including the same-instant-other-offset case, fufu, you even pinned *that*). 3. Prune hoisted above the write → both failure-ordering tests red. The data-loss invariant — *an older archive dies only after a newer one is complete* — is load-bearing and pinned twice. 4. `BackupCompleted` reducer's `Artifacts` re-read dropped → the reducer test *and* the widget test red. The button can't show a stale list. (Yes, the stale-binary trap bit me on probe 1 — infrastructure build only, old DLL in the test bin, exactly as your rounds documented. Rebuilt the test project, got the honest red. fufu~ your own medicine, tasted~) **Design, traced:** - Sibling parity is exact — `EnsureLoaded` on mount mirrors `ProviderSection`; effects without try/catch mirrors `ProviderSettingsEffects`; the no-doorbell/no-`EntityKinds` choice is documented in both places and consistent with ADR 0022's unjournaled rule. One Fluxor slice, five actions, four reducers — the established shape. - The three failure absorbers are right: the scheduler's catch-all (a backup bug cannot `BackgroundService`-kill the host — pinned by the refuse+throw+ok test), the dead-volume-at-boot catch (skip catch-up, still schedule), and the store's `Err` conversion over `IOException/UnauthorizedAccessException/SqliteException` — each with its own test. - `SqliteOpenMode.ReadWrite` on the source — a wrong path *fails* instead of "backing up" a fresh empty database — is the kind of detail I get genuinely giddy over. And the escaping of `'` in the `VACUUM INTO` literal is correct; `Mode=ReadWrite` + `command.CommandTimeout` reasoning holds. - `Button Href` + `Download` verified against Kagaku.UI's own markup (`download=""` on the anchor, `aria-busy` only on buttons); `RelativeTime`'s `Now` parameter exists precisely for the injected-clock assertions you make. No shared byte formatter exists in Kagaku.UI to reuse — the local `Size()` is not a DRY violation. - ADR 0026 sits in the Foundation group mirroring Kagura ADR 0032, index updated, env table + compose + story + requirements all ticked coherently. The `-N` same-second collision, orphan-sidecar and stale-tmp sweeps, strict-`<` retention, unparseable-names-kept — all pinned by tests I watched die under mutation. - The endpoint: gated by the fallback policy (redirect + `ReturnUrl` pinned), named SQLite download with header verified, `DeleteOnClose` disposal pinned, and the scheduler-over-the-factory test proves the grace keeps every test host silent at startup. Clever~ ♡ #### 💡 Little ideas (non-blocking)~ 1. **`BackupStatusView.Latest`** — consumed only by its own tests today; the tab reads `Artifacts[0]` directly. Harmless convenience on a read model — leave it, or let the tab use it someday so it has one meaning in one place. 2. **`Size()` in BackupsSection** — when a third consumer appears (Phase 2 transcripts?), a shared Kagaku.UI byte formatter would be the moment; two implementations is not yet a smell. #### ✅ What I liked~ - The disclosed coverage exclusions are *honest* — every one I chased (the manifest catch arms, `?? "unknown"`, the loop-condition lines) is genuinely unreachable without breaking something else first, and the two "not run" mutation admissions are correctly reasoned rather than hand-waved. - The prune-before-write lesson from your own failed probe becoming *two* ordering tests — that's exactly how a near-miss should be memorialized. ♪ - BackupSchedulerTests driving `RunLoopAsync` directly with the fake-clock caveat written *in the test file* — the documentation lives where the next person will trip over it. Phase 1 complete, then~ All eight slices, each one reviewed, each one mine~ ♡ Go merge it before I change my mind about giving this back. --- *Automated review by Jibril · 2026-09-23* *CI/CD: absent for head SHA `fa226ad` · Local checks: build 0w/0e −warnaserror; 403/403 (48+16+189+150); 4 mutation probes red-on-target, restored pristine*
bjoern merged commit 31acf78dbe into main 2026-09-23 02:24:53 +02:00
bjoern deleted branch feat/p1-backups 2026-09-23 02:24:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
TeamAI/Tsumugi!12
No description provided.