package web import ( "context" "encoding/json" "net/url" "testing" ) // R-39, Scenario B — the hub half of the fix: a re-issue must change the DESCRIPTOR BYTES. // // The agent re-applies only when the descriptor's content hash moves (felhom-agent // internal/pbsdr/manager.go descriptorHash marshals the parsed struct). An ep0 re-key rotates the // secret of an EXISTING token, so token_id / fingerprint / datastore / namespace all come back // byte-identical — which is precisely why the 2026-07-18 N100 box short-circuited forever. The only // field that moves is SecretGeneration. // // COMPANION RED-PROOF (run + recorded in REPORT.md): delete the `SecretGeneration` field from // pbsDRDescriptor (or stop setting it in the re-issue path) → the two marshals below become // byte-identical and this test FAILS, reproducing the defect exactly. func TestPBSDRDescriptor_ReissueChangesTheBytes(t *testing.T) { // The descriptor as it stands after the FIRST provision. before := &pbsDRDescriptor{ Enabled: true, StorageID: "felhom-pbs", PBSTunnelIP: "10.77.0.1", Datastore: "felhom-offsite", Namespace: "demo-felhom", TokenID: "felhom@pbs!demo-felhom", Fingerprint: "c6:07:28:3f", SecretGeneration: 1, } // After a RE-KEY: everything the endpoint returns is identical — only the secret rotated. after := *before after.SecretGeneration = 2 b1, err := json.Marshal(before) if err != nil { t.Fatalf("marshal before: %v", err) } b2, err := json.Marshal(&after) if err != nil { t.Fatalf("marshal after: %v", err) } if string(b1) == string(b2) { t.Fatalf("a re-issue left the descriptor BYTE-IDENTICAL — the agent will short-circuit and never "+ "consume the fresh secret (this is the R-39 defect).\n bytes: %s", b1) } // And the difference must be exactly the generation — not an accident of field ordering. var m1, m2 map[string]any if err := json.Unmarshal(b1, &m1); err != nil { t.Fatal(err) } if err := json.Unmarshal(b2, &m2); err != nil { t.Fatal(err) } for k, v1 := range m1 { if k == "secret_generation" { continue } if v2, ok := m2[k]; !ok || string(mustJSON(t, v1)) != string(mustJSON(t, v2)) { t.Errorf("a re-key must change ONLY secret_generation, but %q moved: %v -> %v", k, v1, m2[k]) } } if m2["secret_generation"] == m1["secret_generation"] { t.Error("secret_generation did not advance") } } // The field must be OMITTED when zero, so a hub that has never minted for a host does not start // emitting a new key into every legacy descriptor (which would itself be a spurious re-apply). func TestPBSDRDescriptor_ZeroGenerationIsOmitted(t *testing.T) { b, err := json.Marshal(&pbsDRDescriptor{Enabled: true, StorageID: "felhom-pbs"}) if err != nil { t.Fatal(err) } var m map[string]any if err := json.Unmarshal(b, &m); err != nil { t.Fatal(err) } if _, present := m["secret_generation"]; present { t.Errorf("zero generation must be omitted (omitempty) — emitting it would move the hash of every "+ "pre-existing descriptor and cause a fleet-wide spurious re-apply. got: %s", b) } } // An UNKNOWN key must survive a round-trip through readPBSDR/mergePBSDR untouched — the Scenario-C // compatibility direction, asserted from the hub side: an old agent's descriptor is not corrupted by // a hub that now writes the new field. func TestPBSDRDescriptor_RoundTripPreservesOtherKeys(t *testing.T) { desired := `{"operator":{"note":"keep me"},"pbs_dr":{"enabled":true,"storage_id":"felhom-pbs","secret_generation":7}}` cur := readPBSDR(desired) if cur == nil { t.Fatal("readPBSDR returned nil") } if cur.SecretGeneration != 7 { t.Fatalf("secret_generation round-trip = %d, want 7", cur.SecretGeneration) } cur.SecretGeneration = 8 merged, err := mergePBSDR(desired, cur) if err != nil { t.Fatalf("merge: %v", err) } var m map[string]json.RawMessage if err := json.Unmarshal([]byte(merged), &m); err != nil { t.Fatal(err) } if _, ok := m["operator"]; !ok { t.Error("merge dropped a sibling key in desired_json") } if got := readPBSDR(merged); got == nil || got.SecretGeneration != 8 { t.Errorf("merged descriptor lost the advanced generation: %+v", got) } } func mustJSON(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return b } // R-39, Scenario B at the FLOW level — the assertion that actually guards the shipped behaviour. // // The struct test above proves the field moves the bytes; this proves ReissuePBSDR *stamps* it. // fakeTenancy returns an identical TokenID / Fingerprint / Datastore / Namespace on every call and // rotates only the secret — which is exactly what an ep0 re-key does, and exactly why the descriptor // used to come back byte-identical. // // COMPANION RED-PROOF (run + recorded): comment out `cur.SecretGeneration = secretGen` in // ReissuePBSDR (keep the field declared so it still compiles) → the pbs_dr block is unchanged across // the re-issue and this test FAILS with both identical blocks printed. That is the 2026-07-18 N100 // behaviour reproduced in a unit test. func TestReissuePBSDR_ChangesTheStoredDescriptor(t *testing.T) { fake := &fakeTenancy{secret: "OLD"} s, st, _ := newPBSDRServer(t, fake) postUpdate(t, s, url.Values{"dr_tier": {"on"}}) // provision → descriptor + generation 1 pbsBlockOf := func(what string) string { t.Helper() h, err := st.GetHost("peti-01") if err != nil || h == nil { t.Fatalf("%s: get host: %v", what, err) } var doc map[string]json.RawMessage if err := json.Unmarshal([]byte(h.DesiredJSON), &doc); err != nil { t.Fatalf("%s: parse desired_json: %v", what, err) } return string(doc["pbs_dr"]) } before := pbsBlockOf("before") if before == "" { t.Fatal("no pbs_dr descriptor after provisioning") } // The agent consumes it and converges — the box is now pinned to this exact descriptor hash. if _, err := st.ConsumeHostPBSSecret("peti-01"); err != nil { t.Fatalf("consume: %v", err) } fake.secret = "FRESH" // the re-key: a new secret behind an unchanged token if err := s.ReissuePBSDR(context.Background(), "peti"); err != nil { t.Fatalf("ReissuePBSDR: %v", err) } after := pbsBlockOf("after") if after == before { t.Fatalf("the re-issue left the pbs_dr descriptor BYTE-IDENTICAL — a converged agent will "+ "short-circuit on its content hash and never consume the fresh secret (R-39).\n block: %s", before) } // The fresh secret must genuinely be re-armed for consumption, and the generation advanced. var b, a pbsDRDescriptor if err := json.Unmarshal([]byte(before), &b); err != nil { t.Fatal(err) } if err := json.Unmarshal([]byte(after), &a); err != nil { t.Fatal(err) } if a.SecretGeneration <= b.SecretGeneration { t.Errorf("secret_generation did not advance across a re-issue: %d -> %d", b.SecretGeneration, a.SecretGeneration) } // Everything else is identical — proving the generation is the ONLY thing carrying the signal. if a.TokenID != b.TokenID || a.Fingerprint != b.Fingerprint || a.Namespace != b.Namespace || a.Datastore != b.Datastore { t.Errorf("this fake models a re-key, so these must be unchanged; if they differ the test is no "+ "longer exercising the defect shape.\n before=%+v\n after=%+v", b, a) } got, err := st.ConsumeHostPBSSecret("peti-01") if err != nil || got != "FRESH" { t.Errorf("post-reissue consume = (%q, %v), want (FRESH, nil) — the fresh secret must be consumable", got, err) } }