ce1a4b4758
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
105 lines
2.7 KiB
Go
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)
|
|
}
|
|
}
|