gates / gates (push) Successful in 47s
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
126 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|