Files
felhom-controller/REPORT.md
T
2026-08-22 13:25:36 +02:00

106 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.