Files
felhom-agent/internal/signedjobs/decommission_test.go
T
admin 5c3b218822
gates / gates (push) Successful in 47s
The signed decommission prunes the drive from every guest's bind record (found live on demo-hp, R-1124 proof)
The self-serve decommission already dropped the drive from the guest-bind record; the operator-signed one did
not, so a forgotten drive left a record the 20-second re-assert warned about forever ("skipping non-enrolled
drive"). DecommissionExecutor.SetGuestBinds; red-proved. Code only; the CHANGELOG follows the release (0.159.1).

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-17 16:41:12 +02:00

126 lines
3.8 KiB
Go

package signedjobs
import (
"context"
"encoding/json"
"testing"
)
type fakeDecommissioner struct {
got string
err error
}
func (f *fakeDecommissioner) SetDecommissioned(durableID string) error {
f.got = durableID
return f.err
}
func TestDecommissionExecutor_HappyPath(t *testing.T) {
fd := &fakeDecommissioner{}
ex := NewDecommissionExecutor(fd, nil)
err := ex.Execute(context.Background(), opDecommission, json.RawMessage(`{"durable_id":"uuid:abc-123"}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if fd.got != "uuid:abc-123" {
t.Errorf("SetDecommissioned got %q, want uuid:abc-123", fd.got)
}
}
func TestDecommissionExecutor_NotOurOp(t *testing.T) {
fd := &fakeDecommissioner{}
ex := NewDecommissionExecutor(fd, nil)
if err := ex.Execute(context.Background(), "storage_wipe", json.RawMessage(`{}`)); err != ErrNoExecutor {
t.Errorf("want ErrNoExecutor for a foreign op, got %v", err)
}
if fd.got != "" {
t.Errorf("SetDecommissioned must not be called for a foreign op (got %q)", fd.got)
}
}
func TestDecommissionExecutor_RefusesUnbound(t *testing.T) {
fd := &fakeDecommissioner{}
ex := NewDecommissionExecutor(fd, nil)
if err := ex.Execute(context.Background(), opDecommission, json.RawMessage(`{"durable_id":""}`)); err == nil {
t.Error("want error for an empty durable_id (unbound decommission)")
}
if fd.got != "" {
t.Errorf("SetDecommissioned must not be called for an unbound op (got %q)", fd.got)
}
}
func TestDecommissionExecutor_NoIntentStore(t *testing.T) {
ex := NewDecommissionExecutor(nil, nil)
if err := ex.Execute(context.Background(), opDecommission, json.RawMessage(`{"durable_id":"uuid:x"}`)); err == nil {
t.Error("want a hard error (not silent success) when no intent store is wired")
}
}
// chainProbe records whether it was reached and what it returns.
type chainProbe struct {
owns string
called bool
ret error
}
func (c *chainProbe) Execute(_ context.Context, op string, _ json.RawMessage) error {
c.called = true
if op == c.owns {
return c.ret
}
return ErrNoExecutor
}
func TestExecutorChain_DispatchesToOwner(t *testing.T) {
a := &chainProbe{owns: "storage_wipe"}
b := &chainProbe{owns: "decommission"}
chain := ExecutorChain{a, b}
if err := chain.Execute(context.Background(), "decommission", nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !b.called {
t.Error("owner (decommission) executor was not reached")
}
// An op no sub-executor owns → ErrNoExecutor (left queued).
if err := chain.Execute(context.Background(), "guest_destroy", nil); err != ErrNoExecutor {
t.Errorf("want ErrNoExecutor for an unowned op, got %v", err)
}
}
type fakeBinds struct{ m map[int][]string }
func (f *fakeBinds) Guests() map[int][]string { return f.m }
func (f *fakeBinds) Remove(vmid int, id string) error {
var kept []string
for _, x := range f.m[vmid] {
if x != id {
kept = append(kept, x)
}
}
f.m[vmid] = kept
return nil
}
// v0.159.1: the signed decommission drops the drive from EVERY guest's bind record, as the self-serve path does — and
// only that drive. RED-PROOF (felhom.eu audits/agent-allowlist-rotation-2026-10-17/P1/redproof-bindprune.txt): with
// the prune loop removed, the record still holds the drive and this test fails.
func TestDecommissionExecutor_PrunesTheGuestBindRecord(t *testing.T) {
fd := &fakeDecommissioner{}
fb := &fakeBinds{m: map[int][]string{9201: {"uuid:keep", "uuid:gone"}, 9202: {"uuid:gone"}}}
ex := NewDecommissionExecutor(fd, nil)
ex.SetGuestBinds(fb)
params, _ := json.Marshal(decommissionParams{DurableID: "uuid:gone"})
if err := ex.Execute(context.Background(), opDecommission, params); err != nil {
t.Fatal(err)
}
if got := fb.m[9201]; len(got) != 1 || got[0] != "uuid:keep" {
t.Fatalf("9201 record = %v, want [uuid:keep]", got)
}
if got := fb.m[9202]; len(got) != 0 {
t.Fatalf("9202 record = %v, want empty", got)
}
}