From 149a3b092e23f932ce0135855e4e243472f4b00f Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 16 Jun 2026 20:25:51 +0200 Subject: [PATCH] =?UTF-8?q?hub=20v0.13.1=20=E2=80=94=20DR=20recipe=20v1=20?= =?UTF-8?q?drive-shape=20sync=20(test-data=20+=20regression=20guard=20only?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracks felhom-agent v0.39.0, which dropped role + restic_repo_coord from the host-half drives[]. Hub reads drives as json.RawMessage (verbatim passthrough) → no store/handler change needed. Dropped role from the store golden + drHostHalf fixture; re-synced the api host-report golden byte-identical with the agent copy (it previously lacked dr_recipe entirely). Added TestAssembleDRRecipe_V1DriveShape regression guard (demonstrated to fail when the fixture re-adds role, then reverted). No behavior change; redeploy optional. Co-Authored-By: Claude Opus 4.8 (1M context) --- hub/CHANGELOG.md | 17 ++++++++++ .../api/testdata/host-report.golden.json | 25 ++++++++++++++- hub/internal/store/dr_recipe_test.go | 31 ++++++++++++++++++- .../store/testdata/dr-recipe.golden.json | 1 - 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index b85b018..e9d311b 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -1,5 +1,22 @@ # Felhom Hub — Changelog +## v0.13.1 — DR recipe v1 drive-shape sync: test-data + regression guard only (2026-06-16) + +**No behavior change — redeploy optional.** Tracks the agent's v0.39.0 v1 host-half drive shape (which +dropped `role` + `restic_repo_coord` from `drives[]`). Because the hub reads drives as `json.RawMessage` +(verbatim passthrough), no store/handler/struct change was needed — only test-data + a regression guard. + +- `internal/store/testdata/dr-recipe.golden.json` + the `drHostHalf` test fixture: dropped the `role` + key from `drives[0]` to match the v1 shape the agent now emits. +- `internal/api/testdata/host-report.golden.json`: re-synced to be **byte-identical** with the agent's + `internal/hub/testdata/host-report.golden.json` (sha256 `57f2a5e7…18b2f2b5`). The hub copy previously + lacked the `dr_recipe` section entirely; it is now a verbatim copy, so the cross-repo golden truly + matches and POSTing it through `/host-report` exercises the `SaveDRRecipeHostHalf` ingest path. +- New `TestAssembleDRRecipe_V1DriveShape` — the regression guard: a stored host half whose `drives[]` + carry NEITHER dropped field but WHICH HAS a `pbs` block assembles cleanly (pbs carried through, drives + passed through verbatim, neither `role` nor `restic_repo_coord` present). Demonstrated to FAIL when the + fixture re-adds `role`, then reverted. + ## v0.13.0 — DR recipe: assemble + store + view the secret-free reconstruction recipe (2026-06-16) **DR recipe slice (hub half)** — the assemble-store-view side of the secret-free reconstruction recipe diff --git a/hub/internal/api/testdata/host-report.golden.json b/hub/internal/api/testdata/host-report.golden.json index 7309266..16832c1 100644 --- a/hub/internal/api/testdata/host-report.golden.json +++ b/hub/internal/api/testdata/host-report.golden.json @@ -131,5 +131,28 @@ } ], "cloudflared": { "status": "active" }, - "audit_tail": [] + "audit_tail": [], + "dr_recipe": { + "recipe_version": 1, + "guests": [ + { "vmid": 100, "cores": 2, "memory_bytes": 2147483648, "disk_bytes": 21474836480 } + ], + "pbs": { + "repo_id": "felhom-pbs", + "namespace": "root", + "latest_snapshot_id": "9001" + }, + "drives": [ + { + "durable_id": "uuid:0fc63daf-8483-4772-8e79-3d69d8477de4", + "mount_path": "/mnt/usb-backup", + "intent": "enrolled", + "total_bytes": 2000000000000 + } + ], + "pve_storage": [ + { "name": "local-lvm", "type": "lvmthin", "content": "rootdir,images" }, + { "name": "usb-backup", "type": "usb", "content": "backup" } + ] + } } diff --git a/hub/internal/store/dr_recipe_test.go b/hub/internal/store/dr_recipe_test.go index 7048e1e..205bafb 100644 --- a/hub/internal/store/dr_recipe_test.go +++ b/hub/internal/store/dr_recipe_test.go @@ -16,7 +16,7 @@ const drHostHalf = `{ "recipe_version": 1, "guests": [ { "vmid": 9201, "cores": 4, "memory_bytes": 12884901888, "disk_bytes": 34359738368 } ], "pbs": { "repo_id": "felhom-pbs", "namespace": "root", "latest_snapshot_id": "9201" }, - "drives": [ { "durable_id": "uuid:da9e7089-cf8e-4617-adcb-a377743fae00", "role": "bulk-data", "mount_path": "/mnt/felhom-usb", "intent": "enrolled", "total_bytes": 1000000000000 } ], + "drives": [ { "durable_id": "uuid:da9e7089-cf8e-4617-adcb-a377743fae00", "mount_path": "/mnt/felhom-usb", "intent": "enrolled", "total_bytes": 1000000000000 } ], "pve_storage": [ { "name": "local-lvm", "type": "lvmthin", "content": "rootdir,images" }, { "name": "felhom-usb", "type": "usb", "content": "backup" } ] }` @@ -104,6 +104,35 @@ func TestAssembleDRRecipe_MatchesGolden(t *testing.T) { } } +// TestAssembleDRRecipe_V1DriveShape is the regression guard for the v1 host-half drive shape +// (agent v0.39.0 dropped role + restic_repo_coord): a stored host half whose drives carry NEITHER +// field, but WHICH HAS a pbs block, must assemble cleanly — pbs present, drives passed through +// verbatim (RawMessage passthrough means the hub needs no struct change for the dropped fields). +func TestAssembleDRRecipe_V1DriveShape(t *testing.T) { + const v1Host = `{ + "recipe_version": 1, + "guests": [ { "vmid": 9201, "cores": 4, "memory_bytes": 12884901888, "disk_bytes": 34359738368 } ], + "pbs": { "repo_id": "felhom-pbs", "namespace": "root", "latest_snapshot_id": "9201" }, + "drives": [ { "durable_id": "uuid:da9e7089", "mount_path": "/mnt/felhom-usb", "intent": "enrolled", "total_bytes": 1000000000000 } ], + "pve_storage": [ { "name": "felhom-usb", "type": "usb", "content": "backup" } ] + }` + asm, err := AssembleDRRecipe(&DRRecipe{CustomerID: "c", RecipeVersion: 1, HostHalfJSON: v1Host}) + if err != nil { + t.Fatalf("v1 host half failed to assemble: %v", err) + } + // pbs must survive the stitch. + if !jsonContains(t, asm.PBS, "felhom-pbs") || !jsonContains(t, asm.PBS, "9201") { + t.Errorf("pbs coord not carried through assembly: %s", asm.PBS) + } + // drives passed through verbatim, and carry NEITHER dropped field. + if !jsonContains(t, asm.Drives, "uuid:da9e7089") { + t.Errorf("drives not passed through: %s", asm.Drives) + } + if strings.Contains(string(asm.Drives), "role") || strings.Contains(string(asm.Drives), "restic_repo_coord") { + t.Errorf("v1 drives must not carry role/restic_repo_coord: %s", asm.Drives) + } +} + // TestAssembleDRRecipe_IgnoreUnknownAndVersionSkew: a half carrying an UNKNOWN top-level field and a // HIGHER recipe_version still assembles (forward-compat), and recipe_version reflects the max. func TestAssembleDRRecipe_IgnoreUnknownAndVersionSkew(t *testing.T) { diff --git a/hub/internal/store/testdata/dr-recipe.golden.json b/hub/internal/store/testdata/dr-recipe.golden.json index e2b4346..42cd50c 100644 --- a/hub/internal/store/testdata/dr-recipe.golden.json +++ b/hub/internal/store/testdata/dr-recipe.golden.json @@ -8,7 +8,6 @@ "drives": [ { "durable_id": "uuid:da9e7089-cf8e-4617-adcb-a377743fae00", - "role": "bulk-data", "mount_path": "/mnt/felhom-usb", "intent": "enrolled", "total_bytes": 1000000000000