Major enhancements: - Added production-grade durability guarantees with fsync operations - Implemented BadgerDB WAL for crash recovery and persistence - Added comprehensive HTTP API (GET/POST /state, POST /command) - Exported ComputeStateHash for external use in genesis initialization - Enhanced snapshot system with atomic write-fsync-rename sequence - Added API integration documentation and durability guarantees docs New files: - api.go: HTTP server implementation with state and command endpoints - api_test.go: Comprehensive API test suite - badger_wal.go: BadgerDB-based write-ahead log - cmd/swoosh/main.go: CLI entry point with API server - API_INTEGRATION.md: API usage and integration guide - DURABILITY.md: Durability guarantees and recovery procedures - CHANGELOG.md: Version history and changes - RELEASE_NOTES.md: Release notes for v1.0.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
284 lines
6.7 KiB
Go
284 lines
6.7 KiB
Go
package swoosh
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHandleTransition(t *testing.T) {
|
|
// Setup executor with in-memory stores
|
|
wal := &mockWAL{}
|
|
snap := &mockSnapshotStore{}
|
|
initialState := OrchestratorState{
|
|
Meta: struct {
|
|
Version string
|
|
SchemaHash string
|
|
}{
|
|
Version: "1.0.0",
|
|
SchemaHash: "test",
|
|
},
|
|
}
|
|
hash, _ := computeStateHash(initialState)
|
|
initialState.StateHash = hash
|
|
|
|
snapshot := Snapshot{
|
|
State: initialState,
|
|
LastAppliedHLC: "0-0-0",
|
|
LastAppliedIndex: 0,
|
|
}
|
|
|
|
executor := NewExecutor(wal, snap, nil, snapshot)
|
|
|
|
// Create test proposal
|
|
proposal := TransitionProposal{
|
|
CurrentStateHash: initialState.StateHash,
|
|
TransitionName: "LICENSE_GRANTED",
|
|
InputsHash: "test-input",
|
|
Signer: "test-signer",
|
|
IdemKey: "test-idem-1",
|
|
HLC: "1-0-0000000000000001",
|
|
WindowID: "window-1",
|
|
Evidence: []string{"test-evidence"},
|
|
}
|
|
|
|
body, _ := json.Marshal(proposal)
|
|
req := httptest.NewRequest(http.MethodPost, "/transition", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := handleTransition(executor)
|
|
handler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if success, ok := response["success"].(bool); !ok || !success {
|
|
t.Errorf("expected success=true, got %v", response)
|
|
}
|
|
|
|
if _, ok := response["state_hash"].(string); !ok {
|
|
t.Errorf("expected state_hash in response")
|
|
}
|
|
}
|
|
|
|
func TestHandleState(t *testing.T) {
|
|
wal := &mockWAL{}
|
|
snap := &mockSnapshotStore{}
|
|
initialState := OrchestratorState{
|
|
HLCLast: "1-0-0000000000000001",
|
|
}
|
|
hash, _ := computeStateHash(initialState)
|
|
initialState.StateHash = hash
|
|
|
|
snapshot := Snapshot{
|
|
State: initialState,
|
|
LastAppliedHLC: initialState.HLCLast,
|
|
LastAppliedIndex: 1,
|
|
}
|
|
|
|
executor := NewExecutor(wal, snap, nil, snapshot)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/state", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := handleState(executor)
|
|
handler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if stateHash, ok := response["state_hash"].(string); !ok || stateHash == "" {
|
|
t.Errorf("expected state_hash, got %v", response)
|
|
}
|
|
|
|
if hlcLast, ok := response["hlc_last"].(string); !ok || hlcLast != "1-0-0000000000000001" {
|
|
t.Errorf("expected hlc_last=1-0-0000000000000001, got %v", response)
|
|
}
|
|
}
|
|
|
|
func TestHandleHealth(t *testing.T) {
|
|
wal := &mockWAL{}
|
|
snap := &mockSnapshotStore{}
|
|
initialState := OrchestratorState{
|
|
Boot: struct {
|
|
Licensed bool
|
|
LicenseExpiry time.Time
|
|
NodeID string
|
|
}{
|
|
Licensed: true,
|
|
NodeID: "test-node",
|
|
},
|
|
Control: struct {
|
|
Paused bool
|
|
Degraded bool
|
|
Recovering bool
|
|
}{
|
|
Degraded: false,
|
|
},
|
|
Policy: struct {
|
|
Quarantined bool
|
|
Rationale string
|
|
}{
|
|
Quarantined: false,
|
|
},
|
|
HLCLast: "5-0-0000000000000005",
|
|
}
|
|
hash, _ := computeStateHash(initialState)
|
|
initialState.StateHash = hash
|
|
|
|
snapshot := Snapshot{
|
|
State: initialState,
|
|
LastAppliedHLC: "5-0-0000000000000005",
|
|
LastAppliedIndex: 5,
|
|
}
|
|
|
|
executor := NewExecutor(wal, snap, nil, snapshot)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := handleHealth(executor)
|
|
handler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if licensed, ok := response["licensed"].(bool); !ok || !licensed {
|
|
t.Errorf("expected licensed=true, got %v", response)
|
|
}
|
|
|
|
if quarantined, ok := response["quarantined"].(bool); !ok || quarantined {
|
|
t.Errorf("expected quarantined=false, got %v", response)
|
|
}
|
|
|
|
if degraded, ok := response["degraded"].(bool); !ok || degraded {
|
|
t.Errorf("expected degraded=false, got %v", response)
|
|
}
|
|
}
|
|
|
|
func TestHandleCouncilOpportunity_NotImplemented(t *testing.T) {
|
|
wal := &mockWAL{}
|
|
snap := &mockSnapshotStore{}
|
|
snapshot := Snapshot{State: OrchestratorState{}}
|
|
executor := NewExecutor(wal, snap, nil, snapshot)
|
|
|
|
payload := map[string]interface{}{
|
|
"council_id": "test-council",
|
|
"roles": []string{"developer", "reviewer"},
|
|
"window_id": "window-1",
|
|
"hlc": "1-0-0000000000000001",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/opportunities/council", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := handleCouncilOpportunity(executor)
|
|
handler(w, req)
|
|
|
|
// Should return 501 Not Implemented per spec
|
|
if w.Code != http.StatusNotImplemented {
|
|
t.Errorf("expected status 501, got %d", w.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if _, ok := response["error"].(string); !ok {
|
|
t.Errorf("expected error message in response")
|
|
}
|
|
}
|
|
|
|
func TestHandleTasks_NotImplemented(t *testing.T) {
|
|
wal := &mockWAL{}
|
|
snap := &mockSnapshotStore{}
|
|
snapshot := Snapshot{State: OrchestratorState{}}
|
|
executor := NewExecutor(wal, snap, nil, snapshot)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := handleTasks(executor)
|
|
handler(w, req)
|
|
|
|
// Should return 501 Not Implemented per spec
|
|
if w.Code != http.StatusNotImplemented {
|
|
t.Errorf("expected status 501, got %d", w.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if _, ok := response["error"].(string); !ok {
|
|
t.Errorf("expected error message in response")
|
|
}
|
|
}
|
|
|
|
// Mock implementations for testing
|
|
|
|
type mockWAL struct {
|
|
records []WALRecord
|
|
}
|
|
|
|
func (m *mockWAL) Append(record WALRecord) error {
|
|
m.records = append(m.records, record)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWAL) Replay(fromIndex uint64) ([]WALRecord, error) {
|
|
return []WALRecord{}, nil
|
|
}
|
|
|
|
func (m *mockWAL) Sync() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWAL) LastIndex() uint64 {
|
|
if len(m.records) == 0 {
|
|
return 0
|
|
}
|
|
return m.records[len(m.records)-1].Index
|
|
}
|
|
|
|
type mockSnapshotStore struct {
|
|
latest *Snapshot
|
|
}
|
|
|
|
func (m *mockSnapshotStore) Save(s Snapshot) error {
|
|
m.latest = &s
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSnapshotStore) LoadLatest() (Snapshot, error) {
|
|
if m.latest == nil {
|
|
return Snapshot{}, fmt.Errorf("no snapshot")
|
|
}
|
|
return *m.latest, nil
|
|
}
|