hub v0.13.1 — DR recipe v1 drive-shape sync (test-data + regression guard only)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 20:25:51 +02:00
parent 5f5e3c54a1
commit 149a3b092e
4 changed files with 71 additions and 3 deletions
+17
View File
@@ -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
+24 -1
View File
@@ -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" }
]
}
}
+30 -1
View File
@@ -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) {
-1
View File
@@ -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