Files
felhom-agent/internal/pvegate/pvegate_test.go
T
admin ce1a4b4758 R-812 option A: the Proxmox package lane (layer pve) + the /etc/pve write gate
The wrapper gains layer "pve" (slow lane): the host's Proxmox userspace
packages only — origin "Proxmox Debian Repository", never a kernel / boot /
firmware / microcode name (R14), no removal, no undo, a new package only from
an allow-list; authority = a signed os_pve_step or the root-owned ring-0 mark.
The night leg runs it in ring 0 after a healthy host step; ring 1 only by a
signed job (PVEStepExecutor). While it runs, the agent's own /etc/pve writes
(every non-GET API call, pct config verbs, pvesm, pveum, felhom-pbs-apply)
wait on internal/pvegate. Health = the host rule + unchanged container ids +
pveversion reads the installed pve-manager.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-07 10:19:38 +02:00

105 lines
2.7 KiB
Go

package pvegate
import (
"context"
"testing"
"time"
)
// A write that starts while a step runs waits until the step ends.
//
// COMPANION RED-PROOF (observed): make Write ignore `stepping` → this fails with "the write went through while the
// Proxmox step held the gate". Restored. (audits/day-2026-10-07/B/red-pvegate.txt)
func TestWrite_WaitsWhileAStepRuns(t *testing.T) {
end, err := Step(context.Background())
if err != nil {
t.Fatal(err)
}
got := make(chan time.Time, 1)
go func() {
rel, _, err := Write(context.Background())
if err == nil {
rel()
}
got <- time.Now()
}()
select {
case <-got:
end()
t.Fatal("the write went through while the Proxmox step held the gate")
case <-time.After(150 * time.Millisecond):
}
ended := time.Now()
end()
select {
case at := <-got:
if at.Before(ended) {
t.Fatal("the write finished before the step ended")
}
case <-time.After(2 * time.Second):
t.Fatal("the write never went through after the step ended")
}
}
// A step waits for a write already in flight before it starts.
func TestStep_WaitsForAWriteInFlight(t *testing.T) {
rel, _, err := Write(context.Background())
if err != nil {
t.Fatal(err)
}
started := make(chan struct{})
go func() {
end, err := Step(context.Background())
if err == nil {
close(started)
end()
}
}()
select {
case <-started:
rel()
t.Fatal("the step started while a write was in flight")
case <-time.After(150 * time.Millisecond):
}
rel()
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("the step never started after the write finished")
}
}
// A step that cannot drain the writes in time gives up and releases the gate (it never waits forever).
func TestStep_GivesUpAndReleases(t *testing.T) {
rel, _, _ := Write(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
if _, err := Step(ctx); err == nil {
t.Fatal("the step must give up while a write is in flight past its deadline")
}
if Stepping() {
t.Fatal("a step that gave up must release the gate")
}
rel()
}
// A write held back past its own deadline returns the context's error.
func TestWrite_HonoursItsContext(t *testing.T) {
end, _ := Step(context.Background())
defer end()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
if _, _, err := Write(ctx); err == nil {
t.Fatal("a write held back past its deadline must fail")
}
}
// One step at a time.
func TestStep_OneAtATime(t *testing.T) {
end, _ := Step(context.Background())
defer end()
if _, err := Step(context.Background()); err != ErrStepRunning {
t.Fatalf("a second step must be refused, got %v", err)
}
}