Initial SWOOSH executor and reducer implementation
This commit is contained in:
152
determinism_test.go
Normal file
152
determinism_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package swoosh
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type staticGuardProvider struct {
|
||||
outcomes map[string]GuardOutcome
|
||||
}
|
||||
|
||||
func newStaticGuardProvider() *staticGuardProvider {
|
||||
return &staticGuardProvider{outcomes: make(map[string]GuardOutcome)}
|
||||
}
|
||||
|
||||
func (g *staticGuardProvider) setOutcome(name string, outcome GuardOutcome) {
|
||||
g.outcomes[name] = outcome
|
||||
}
|
||||
|
||||
func (g *staticGuardProvider) Evaluate(t TransitionProposal, s OrchestratorState) (GuardOutcome, error) {
|
||||
if outcome, ok := g.outcomes[t.TransitionName]; ok {
|
||||
return outcome, nil
|
||||
}
|
||||
return GuardOutcome{
|
||||
LicenseOK: true,
|
||||
BackbeatOK: true,
|
||||
QuorumOK: true,
|
||||
PolicyOK: true,
|
||||
MCPHealthy: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTestExecutor(t *testing.T, guard GuardProvider, wal WALStore, snap SnapshotStore) *Executor {
|
||||
t.Helper()
|
||||
initial := Snapshot{}
|
||||
exec := NewExecutor(wal, snap, guard, initial)
|
||||
return exec
|
||||
}
|
||||
|
||||
func waitResult(t *testing.T, ch <-chan ApplyResult) ApplyResult {
|
||||
t.Helper()
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatalf("timed out waiting for apply result")
|
||||
return ApplyResult{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicReplay(t *testing.T) {
|
||||
walStore := &InMemoryWAL{}
|
||||
snapStore := &InMemorySnapshotStore{}
|
||||
guard := newStaticGuardProvider()
|
||||
guard.setOutcome("TRANSITION_QUARANTINE_ENTER", GuardOutcome{PolicyOK: false})
|
||||
|
||||
exec := newTestExecutor(t, guard, walStore, snapStore)
|
||||
|
||||
transitions := []TransitionProposal{
|
||||
{TransitionName: "LICENSE_GRANTED", HLC: "1"},
|
||||
{TransitionName: "INGESTION_SOURCES_RESOLVED", HLC: "2"},
|
||||
{TransitionName: "INGESTION_BYTES_OK", HLC: "3"},
|
||||
{TransitionName: "INGESTION_SCHEMA_OK", HLC: "4"},
|
||||
{TransitionName: "INGESTION_CORPUS_BUILT", HLC: "5"},
|
||||
{TransitionName: "COUNCIL_PROFILES_LOADED", HLC: "6"},
|
||||
{TransitionName: "COUNCIL_QUORUM_CERT", HLC: "7"},
|
||||
{TransitionName: "COUNCIL_MCP_GREEN", HLC: "8"},
|
||||
{TransitionName: "ENV_CAPACITY_OK", HLC: "9"},
|
||||
{TransitionName: "ENV_INSTALLED", HLC: "10"},
|
||||
{TransitionName: "ENV_HEALTH_GREEN", HLC: "11"},
|
||||
{TransitionName: "EXEC_PLAN_LOCKED", HLC: "12", WindowID: "window-1"},
|
||||
{TransitionName: "EXEC_BEAT_REVIEW_GATE", HLC: "13", WindowID: "window-1"},
|
||||
{TransitionName: "EXEC_APPROVALS_THRESHOLD", HLC: "14", WindowID: "window-1"},
|
||||
{TransitionName: "EXEC_NEXT_WINDOW", HLC: "15", WindowID: "window-2"},
|
||||
}
|
||||
|
||||
var finalState OrchestratorState
|
||||
for _, proposal := range transitions {
|
||||
ch, err := exec.SubmitTransition(proposal)
|
||||
if err != nil {
|
||||
t.Fatalf("submit transition %s: %v", proposal.TransitionName, err)
|
||||
}
|
||||
res := waitResult(t, ch)
|
||||
if !res.Success || res.Error != nil {
|
||||
t.Fatalf("transition %s failed: success=%t error=%v", proposal.TransitionName, res.Success, res.Error)
|
||||
}
|
||||
finalState = res.NewState
|
||||
}
|
||||
|
||||
finalHash := finalState.StateHash
|
||||
if finalHash == "" {
|
||||
t.Fatal("final state hash empty")
|
||||
}
|
||||
|
||||
snapshot := Snapshot{
|
||||
State: finalState,
|
||||
LastAppliedHLC: finalState.HLCLast,
|
||||
LastAppliedIndex: walStore.LastIndex(),
|
||||
}
|
||||
if err := snapStore.Save(snapshot); err != nil {
|
||||
t.Fatalf("save snapshot: %v", err)
|
||||
}
|
||||
|
||||
replayState, err := Replay(walStore, snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("replay failed: %v", err)
|
||||
}
|
||||
|
||||
if replayState.StateHash != finalHash {
|
||||
t.Fatalf("state hash mismatch after replay: got %s want %s", replayState.StateHash, finalHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuarantineEnforced(t *testing.T) {
|
||||
walStore := &InMemoryWAL{}
|
||||
snapStore := &InMemorySnapshotStore{}
|
||||
guard := newStaticGuardProvider()
|
||||
guard.setOutcome("TRANSITION_QUARANTINE_ENTER", GuardOutcome{PolicyOK: false})
|
||||
|
||||
exec := newTestExecutor(t, guard, walStore, snapStore)
|
||||
|
||||
enterProposal := TransitionProposal{TransitionName: "TRANSITION_QUARANTINE_ENTER", HLC: "1"}
|
||||
ch, err := exec.SubmitTransition(enterProposal)
|
||||
if err != nil {
|
||||
t.Fatalf("submit quarantine enter: %v", err)
|
||||
}
|
||||
res := waitResult(t, ch)
|
||||
if !res.Success || res.Error != nil {
|
||||
t.Fatalf("quarantine enter failed: %v", res.Error)
|
||||
}
|
||||
|
||||
quarantinedState := res.NewState
|
||||
if !quarantinedState.Policy.Quarantined {
|
||||
t.Fatal("state not quarantined after enter transition")
|
||||
}
|
||||
hashBefore := quarantinedState.StateHash
|
||||
|
||||
illegalProposal := TransitionProposal{TransitionName: "EXEC_BEAT_REVIEW_GATE", HLC: "2", WindowID: "window-1"}
|
||||
ch, err = exec.SubmitTransition(illegalProposal)
|
||||
if err != nil {
|
||||
t.Fatalf("submit illegal transition: %v", err)
|
||||
}
|
||||
res = waitResult(t, ch)
|
||||
if res.Success || res.Error == nil {
|
||||
t.Fatalf("expected failure for illegal transition in quarantine, got success=%t error=%v", res.Success, res.Error)
|
||||
}
|
||||
|
||||
stateAfter := exec.GetStateSnapshot()
|
||||
if stateAfter.StateHash != hashBefore {
|
||||
t.Fatalf("state hash changed after illegal transition: got %s want %s", stateAfter.StateHash, hashBefore)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user