Files
felhom-agent/internal/hub/desired_contract_test.go
T
admin 8ecf8929fb slice 10A: activate the control envelope (Down channel) + hub-backed desired provider (v0.15.0)
The control envelope becomes live: the agent caches the hub's desired-state +
generation and re-fetches GET /hosts/{id}/desired-state only when the
generation advances. A new internal/desired Syncer maps the wire shape into a
reconcile.CachingProvider feeding the engine; benign deltas reconcile, an
explicit guest decommission is gated pending_signature (exec is 10B). Adds the
DesiredStateResponse/WireDesiredState wire types + Client.FetchDesiredState +
the loop EnvelopeObserver seam. Cross-repo golden (envelope + desired-state)
byte-identical with the hub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:02:59 +02:00

76 lines
3.0 KiB
Go

package hub
import (
"encoding/json"
"os"
"testing"
)
// The desired-state wire is a contract DUPLICATED across two repos (no shared types module yet).
// testdata/desired-state.golden.json and testdata/control-envelope.golden.json MUST be kept
// byte-identical with felhom.eu/hub's copies; these tests decode them through the agent structs
// and key-set-compare, catching drift between the struct and the served shape.
func TestDesiredStateGolden_DecodesAndKeySet(t *testing.T) {
raw, err := os.ReadFile("testdata/desired-state.golden.json")
if err != nil {
t.Fatal(err)
}
var resp DesiredStateResponse
if err := json.Unmarshal(raw, &resp); err != nil {
t.Fatalf("golden does not decode into DesiredStateResponse: %v", err)
}
if resp.Generation != 4 {
t.Errorf("generation = %d, want 4", resp.Generation)
}
if len(resp.DesiredState.Guests) != 2 {
t.Fatalf("guests = %d, want 2", len(resp.DesiredState.Guests))
}
benign := resp.DesiredState.Guests[0]
if benign.VMID != 100 || benign.Run != "running" || benign.Spec == nil || benign.Spec.Cores != 2 || benign.Description == nil {
t.Errorf("benign guest = %+v", benign)
}
destructive := resp.DesiredState.Guests[1]
if destructive.VMID != 200 || !destructive.Decommission {
t.Errorf("destructive guest = %+v, want vmid 200 decommission", destructive)
}
if resp.DesiredState.PBSNamespace != "felhom-cust-acme" {
t.Errorf("pbs_namespace = %q", resp.DesiredState.PBSNamespace)
}
if resp.DesiredState.RestoreDirective == nil || resp.DesiredState.RestoreDirective.Mode != "guest_loss" {
t.Errorf("restore_directive = %+v, want carried (guest_loss)", resp.DesiredState.RestoreDirective)
}
// Bidirectional key-set drift guard: the marshaled struct's keys must match the golden's
// (top-level, the desired_state object, and a guest element).
var golden map[string]any
json.Unmarshal(raw, &golden)
b, _ := json.Marshal(resp)
var got map[string]any
json.Unmarshal(b, &got)
assertSameKeys(t, "<desired-state top>", golden, got)
assertSameKeys(t, "desired_state", golden["desired_state"], got["desired_state"])
assertSameKeys(t, "desired_state.guests[0]",
firstElem(golden["desired_state"].(map[string]any)["guests"]),
firstElem(got["desired_state"].(map[string]any)["guests"]))
assertSameKeys(t, "desired_state.restore_directive",
golden["desired_state"].(map[string]any)["restore_directive"],
got["desired_state"].(map[string]any)["restore_directive"])
}
func TestControlEnvelopeGolden_Decodes(t *testing.T) {
raw, err := os.ReadFile("testdata/control-envelope.golden.json")
if err != nil {
t.Fatal(err)
}
var env ControlEnvelope
if err := json.Unmarshal(raw, &env); err != nil {
t.Fatalf("golden does not decode into ControlEnvelope: %v", err)
}
if env.Status != "ok" || env.PollIntervalSeconds == nil || *env.PollIntervalSeconds != 900 {
t.Errorf("envelope poll/status = %+v", env)
}
if env.DesiredGeneration != 4 || !env.HasSignedOps || env.Blocked {
t.Errorf("envelope flags = gen %d signed %v blocked %v", env.DesiredGeneration, env.HasSignedOps, env.Blocked)
}
}