106 lines
6.2 KiB
Markdown
106 lines
6.2 KiB
Markdown
# REPORT — R-356: the off-site restore refused every app that has no data drive
|
||
|
||
**Controller v0.219.0**, shipped and proven live on `demo-hp` 2026-08-22.
|
||
|
||
## Baselines used (re-verified at session start)
|
||
|
||
| repo | `main` @ | version | → |
|
||
|---|---|---|---|
|
||
| felhom-controller | `2da259af38cc` | v0.218.0 | **v0.219.0** |
|
||
| felhom-agent | `40d857b52711` | v0.130.0 | unchanged |
|
||
| felhom.eu (hub) | `f2edf7e54595` | v0.106.0 | unchanged |
|
||
| app-catalog | `459766cb1639` | — | unchanged |
|
||
|
||
**MinAgent stays 0.129.0.** All four hashes matched the prompt.
|
||
|
||
**The catalogue count, measured in this session and not taken from the prompt:** 53 templates,
|
||
**13 declare `needs_hdd: true`**, **40 declare `false`**, and exactly 13 carry a `backup:` block.
|
||
Agrees with §1.
|
||
|
||
## The defect
|
||
|
||
`ReconstituteFromOffsite` and `PlaceOffsiteRestore` both resolved the restore destination with the
|
||
**raw** `stackProvider.GetStackHDDPath(stack)` and refused when it was empty, with a message saying
|
||
the app is not installed. For the 40 driveless apps that value is never set, so both refused
|
||
permanently for a running, healthy app — and the remedy they offered ("reinstall it in the same
|
||
place") cannot be carried out, because those apps offer no place to pick.
|
||
|
||
The capture side never had this: `CaptureRecoveryUnit` resolves via `GetAppDrivePath`, which falls
|
||
back to `systemDataPath`. That is why the 2026-08-21 refusal could print `/mnt/sys_drive` — a
|
||
destination the backup had recorded and the restore refused to use.
|
||
|
||
## The change
|
||
|
||
- `internal/backup/backup.go` — new `Manager.isStackDeployed`, built on `knownStackNames()` →
|
||
`ListDeployedStacks()`. **Nil provider ⇒ false**, documented with its reason: the caller's next act
|
||
is a write, so "cannot tell" must fail into the recoverable refusal, not into a copy.
|
||
- `internal/backup/offbox_reconstitute.go` — the refusal now asks *installed?* first (R-351 sentence
|
||
and the recorded-drive half intact), then resolves the destination with `GetAppDrivePath`, then has
|
||
a **third, distinct** refusal for installed-but-no-resolvable-data-root. R-253 and R-351 comments
|
||
kept and extended with what the refusal stopped covering and why.
|
||
- `internal/backup/offbox_restore.go` — the same four steps in `PlaceOffsiteRestore`. The headroom
|
||
gate below it then measures the resolved namespace, which is the correct disk in both cases.
|
||
|
||
**Not changed, deliberately:** `offboxCaptureSet`'s raw `GetStackHDDPath` (`offbox_capture.go:43`) and
|
||
`offboxRestoreScratchDir`.
|
||
|
||
**`ListDeployedStacks()` semantics were verified, not assumed.** Source: `stackAdapter` skips
|
||
`!s.Deployed` (`cmd/controller/main.go:2138`). Test: positive and negative controls in
|
||
`TestR356_IsStackDeployedIsExactAndFailsClosed`. Seam: an AST walk in
|
||
`cmd/controller/r356_deployed_seam_test.go` pins both the `SetStackProvider` wiring in `main()` and
|
||
the `!s.Deployed` guard itself.
|
||
|
||
## Tests
|
||
|
||
New: `internal/backup/r356_hot_only_restore_test.go` (10 tests, Scenarios A–E across both entry
|
||
points) and `cmd/controller/r356_deployed_seam_test.go` (2 AST tests). **Test count in
|
||
`internal/backup` 315 → 325, `cmd/controller` 42 → 44.**
|
||
|
||
**Every red-proof was SEEN failing.** Mutation → observed failure:
|
||
|
||
| # | mutation applied | observed |
|
||
|---|---|---|
|
||
| A | destination back to `stackProvider.GetStackHDDPath` | `ScenarioA` failed: *"a(z) immich telepítve van, de a vezérlő nem tudja megállapítani, hová tartoznak az adatai…"* |
|
||
| B | `GetAppDrivePath` always returns `systemDataPath` | 3 tests failed, incl. *"placement …/sys/felhom-data/… left the app's own drive"* — this is also the **positive control** for the "13-class unchanged" absence claim |
|
||
| C | `isStackDeployed` always true | 3 tests failed; the not-installed refusal was replaced by a placement-mismatch prompt |
|
||
| D | widen `nincs telepítve` to cover the no-data-root case | both ScenarioD tests failed: *"a running app must not be told it is not installed"* |
|
||
| E | revert `PlaceOffsiteRestore` only (fix one entry point) | `ScenarioE_PlaceDriveless` failed |
|
||
|
||
**No red-proof passed.** Green gate: `go build ./... && go vet ./... && go test ./...` — all green.
|
||
|
||
**Fixture correction, stated rather than hidden:** several existing fixtures marked an app "installed"
|
||
by giving it an HDD path — the exact conflation this change removes. 17 tests failed on that alone
|
||
after the change; they now state deployment as its own fact (`prov.deployed`). No assertion was
|
||
weakened, and `TestPlace_UndeployedRefused` was passing for the wrong reason before.
|
||
|
||
## Live walk on `demo-hp` (controller v0.219.0, healthy)
|
||
|
||
Endpoint-level — `claude-in-chrome` is not available on DooPlex. Full record and every artefact:
|
||
`felhom.eu/documentation/audits/DRILL-r356-hot-only-restore-2026-08-22/`.
|
||
|
||
**Driveless (`privatebin`):** planted through the app's own JSON API + a direct file plant with a
|
||
Hungarian accented name; findability proved by plant→find→remove→fail-to-find; off-sited; **11 files
|
||
deleted outright**; scratch prepared (snapshot `306accff`, **unit-only** — the shape never driven to
|
||
completion before); `POST /backup/offbox/reconstitute` — **no refusal**; **15/15 files back byte for
|
||
byte**. Message (160 bytes): *„A(z) privatebin: 0 fájl és 1 adatkötet visszaállítva (mentés:
|
||
2026-08-22 13:14) — az alkalmazás újraindult. Ennek az alkalmazásnak nincs adatbázisa."* It **names
|
||
the volume**, so it is not the R-354 shape.
|
||
|
||
**Drive app (`calibre-web`):** same walk, **3/3 byte-identical**, placed on `/mnt/felhom-drives/hdd_1`;
|
||
`/mnt/sys_drive/felhom-data/userdata/media` does not exist, so nothing was misplaced onto the SSD.
|
||
|
||
**Every downstream leg held on first run.** No finding raised against them.
|
||
|
||
## Also in this session
|
||
|
||
CI run **387** (job 386) failed on `instructions_gate` — **a real gate bug, not mine**: `ef6ac6f` in
|
||
`felhom.eu` compressed closed rows into `CLOSED-ITEMS.md`, which the gate does not read, so every
|
||
citation of a compressed item became "a reference to nothing". Fixed in
|
||
`felhom.eu/scripts/instructions_gate.py` (commit `e18668f`); both controls still convict.
|
||
|
||
## Operator follow-up
|
||
|
||
1. Bake and **vouch** a golden carrying controller 0.219.0. The `golden-currency` gate is red until
|
||
then, correctly: a machine installed now still receives 0.218.0.
|
||
2. **Then** raise the update floor to 0.219.0 — last, in a separate save.
|