Release v1.0.0: Production-ready SWOOSH with durability guarantees
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>
This commit is contained in:
283
api_test.go
Normal file
283
api_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user