stemedb/sdk/go/steme/integration_test.go
jordan 1ce4004807 feat: Complete Phase 2 (The Cortex) - query, lens, and API layers
This commit adds the read path (Cortex) to complement the write path (Spine):

## Crates
- stemedb-api: HTTP API with axum + utoipa OpenAPI
  - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit
  - Metered endpoints with quota enforcement
  - Ed25519 signature verification
- stemedb-lens: Truth resolution lenses
  - RecencyLens, ConsensusLens, ConfidenceLens
  - VoteAwareConsensusLens (Ballot Box pattern)
  - TrustAwareAuthorityLens (The Hive pattern)
  - SkepticLens (conflict analysis)
  - EpochAwareLens (paradigm-safe queries)
- stemedb-query: Query engine with materialized views

## Storage Extensions
- VoteStore: Vote aggregation with cached counts
- TrustRankStore: Agent reputation with decay
- AuditStore: Query audit trail
- IndexStore: SP/P/S index structures
- SupersessionStore: Epoch supersession chains

## SDKs
- sdk/go/steme: Go HTTP client with Ed25519 signing
- sdk/go/adk: ADK-Go tools for AI agents

## Documentation
- Updated CLAUDE.md, architecture.md, roadmap.md
- New ai-lookup entries for all services
- Use case docs for consumer health intelligence
- Arena roadmap for simulation advancement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:22:44 -07:00

715 lines
18 KiB
Go

//go:build integration
package steme
import (
"context"
"fmt"
"os"
"testing"
"time"
)
// Integration tests that run against a live StemeDB server.
//
// To run these tests:
//
// 1. Start the StemeDB API server:
// cargo run --bin stemedb-api
//
// 2. Run the integration tests:
// STEMEDB_URL=http://localhost:3000 go test -tags=integration ./...
//
// These tests are skipped by default (build tag "integration") to avoid
// breaking CI when no server is running.
// getTestClient creates a client with a generated signer for testing.
//
// Skips the test if STEMEDB_URL is not set.
func getTestClient(t *testing.T) *Client {
t.Helper()
baseURL := os.Getenv("STEMEDB_URL")
if baseURL == "" {
t.Skip("STEMEDB_URL not set - skipping integration test")
}
signer, err := GenerateSigner()
if err != nil {
t.Fatalf("GenerateSigner() failed: %v", err)
}
return NewClient(baseURL, signer)
}
// uniqueSubject generates a unique subject for test isolation.
//
// This prevents tests from interfering with each other by ensuring
// each test operates on distinct subjects.
func uniqueSubject(base string) string {
return fmt.Sprintf("%s_%d", base, time.Now().UnixNano())
}
// TestIntegration_Health verifies the health endpoint works.
func TestIntegration_Health(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
health, err := client.Health(ctx)
if err != nil {
t.Fatalf("Health() failed: %v", err)
}
if health.Status != "healthy" {
t.Errorf("Health.Status = %s, want healthy", health.Status)
}
if health.Version == "" {
t.Error("Health.Version is empty")
}
t.Logf("Server status: %s, version: %s, assertions: %d",
health.Status, health.Version, health.AssertionsCount)
}
// TestIntegration_Assert_Query_Roundtrip tests creating an assertion and querying it back.
func TestIntegration_Assert_Query_Roundtrip(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
// Create a unique subject to avoid interference
subject := uniqueSubject("IntegrationTest_Entity")
predicate := "has_value"
expectedValue := "test_value_roundtrip"
// Create assertion
assertion := NewAssertion(subject, predicate).
WithText(expectedValue).
WithConfidence(0.95).
WithLifecycle(LifecycleApproved).
WithSourceClass(SourceClassClinical).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
hash, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert() failed: %v", err)
}
if len(hash) != 64 {
t.Errorf("Assert() hash length = %d, want 64", len(hash))
}
t.Logf("Created assertion with hash: %s", hash)
// Small delay to ensure indexing completes
time.Sleep(100 * time.Millisecond)
// Query it back
params := NewQuery().
WithSubject(subject).
WithPredicate(predicate).
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() failed: %v", err)
}
if result.TotalCount == 0 {
t.Fatal("Query() returned no results")
}
// Verify we got our assertion back
found := false
for _, a := range result.Assertions {
if a.Hash == hash {
found = true
if a.Subject != subject {
t.Errorf("Subject = %s, want %s", a.Subject, subject)
}
if a.Predicate != predicate {
t.Errorf("Predicate = %s, want %s", a.Predicate, predicate)
}
if a.Object.Type != "Text" {
t.Errorf("Object.Type = %s, want Text", a.Object.Type)
}
if a.Object.Value != expectedValue {
t.Errorf("Object.Value = %v, want %s", a.Object.Value, expectedValue)
}
if a.Confidence != 0.95 {
t.Errorf("Confidence = %f, want 0.95", a.Confidence)
}
if a.Lifecycle != LifecycleApproved {
t.Errorf("Lifecycle = %s, want Approved", a.Lifecycle)
}
if a.SourceClass != SourceClassClinical {
t.Errorf("SourceClass = %s, want Clinical", a.SourceClass)
}
if len(a.Signatures) == 0 {
t.Error("Assertion has no signatures")
}
break
}
}
if !found {
t.Errorf("Did not find assertion with hash %s in query results", hash)
}
}
// TestIntegration_Query_WithLens tests querying with different lenses.
func TestIntegration_Query_WithLens(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
// Create a unique subject for this test
subject := uniqueSubject("IntegrationTest_Lens")
predicate := "has_revenue"
// Create multiple assertions with different values
assertions := []struct {
value float64
confidence float64
}{
{100.0, 0.9},
{105.0, 0.95},
{100.0, 0.85}, // Duplicate value to test consensus
}
for i, a := range assertions {
assertion := NewAssertion(subject, predicate).
WithNumber(a.value).
WithConfidence(a.confidence).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
hash, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert(%d) failed: %v", i, err)
}
t.Logf("Created assertion %d with hash: %s (value: %.1f, confidence: %.2f)",
i, hash, a.value, a.confidence)
}
// Small delay for indexing
time.Sleep(100 * time.Millisecond)
// Test different lenses
lensTests := []struct {
name string
lens Lens
}{
{"Recency", LensRecency},
{"Consensus", LensConsensus},
{"Authority", LensAuthority},
}
for _, tt := range lensTests {
t.Run(tt.name, func(t *testing.T) {
params := NewQuery().
WithSubject(subject).
WithPredicate(predicate).
WithLens(tt.lens).
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() with lens %s failed: %v", tt.lens, err)
}
if result.TotalCount == 0 {
t.Errorf("Query() with lens %s returned no results", tt.lens)
}
t.Logf("Lens %s returned %d results", tt.lens, result.TotalCount)
// Verify all results match our subject/predicate
for _, a := range result.Assertions {
if a.Subject != subject {
t.Errorf("Result has wrong subject: %s, want %s", a.Subject, subject)
}
if a.Predicate != predicate {
t.Errorf("Result has wrong predicate: %s, want %s", a.Predicate, predicate)
}
}
})
}
}
// TestIntegration_Query_NotFound tests querying for non-existent data.
func TestIntegration_Query_NotFound(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
// Query for a subject that definitely doesn't exist
subject := uniqueSubject("IntegrationTest_NonExistent")
params := NewQuery().
WithSubject(subject).
WithPredicate("does_not_exist").
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() failed: %v", err)
}
if result.TotalCount != 0 {
t.Errorf("Query() for non-existent data returned %d results, want 0", result.TotalCount)
}
if len(result.Assertions) != 0 {
t.Errorf("Query() for non-existent data returned %d assertions, want 0", len(result.Assertions))
}
if result.HasMore {
t.Error("Query() for non-existent data claims HasMore = true")
}
}
// TestIntegration_Query_Limit tests the limit parameter.
func TestIntegration_Query_Limit(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
subject := uniqueSubject("IntegrationTest_Limit")
predicate := "has_index"
// Create 5 assertions
const numAssertions = 5
for i := 0; i < numAssertions; i++ {
assertion := NewAssertion(subject, predicate).
WithNumber(float64(i)).
WithConfidence(0.9).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
_, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert(%d) failed: %v", i, err)
}
}
// Small delay for indexing
time.Sleep(100 * time.Millisecond)
// Query with limit of 2
params := NewQuery().
WithSubject(subject).
WithPredicate(predicate).
WithLimit(2).
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() failed: %v", err)
}
if len(result.Assertions) > 2 {
t.Errorf("Query() with limit=2 returned %d results, want at most 2", len(result.Assertions))
}
if result.TotalCount > 2 && !result.HasMore {
t.Error("Query() should indicate HasMore when total > limit")
}
}
// TestIntegration_Assert_AllObjectTypes tests all object value types.
func TestIntegration_Assert_AllObjectTypes(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
subject := uniqueSubject("IntegrationTest_Types")
tests := []struct {
name string
predicate string
builder func(*AssertionBuilder) *AssertionBuilder
checkType string
}{
{
name: "Text",
predicate: "has_text",
builder: func(b *AssertionBuilder) *AssertionBuilder {
return b.WithText("hello world")
},
checkType: "Text",
},
{
name: "Number",
predicate: "has_number",
builder: func(b *AssertionBuilder) *AssertionBuilder {
return b.WithNumber(42.5)
},
checkType: "Number",
},
{
name: "Boolean",
predicate: "has_boolean",
builder: func(b *AssertionBuilder) *AssertionBuilder {
return b.WithBoolean(true)
},
checkType: "Boolean",
},
{
name: "Reference",
predicate: "has_reference",
builder: func(b *AssertionBuilder) *AssertionBuilder {
return b.WithReference("Entity_123")
},
checkType: "Reference",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := NewAssertion(subject, tt.predicate).
WithConfidence(0.9).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000")
builder = tt.builder(builder)
assertion := builder.Build()
hash, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert() for %s failed: %v", tt.name, err)
}
t.Logf("Created %s assertion with hash: %s", tt.name, hash)
// Small delay for indexing
time.Sleep(100 * time.Millisecond)
// Query it back
params := NewQuery().
WithSubject(subject).
WithPredicate(tt.predicate).
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() for %s failed: %v", tt.name, err)
}
if result.TotalCount == 0 {
t.Fatalf("Query() for %s returned no results", tt.name)
}
// Verify object type
if result.Assertions[0].Object.Type != tt.checkType {
t.Errorf("Object.Type = %s, want %s", result.Assertions[0].Object.Type, tt.checkType)
}
})
}
}
// TestIntegration_Assert_InvalidAssertion tests that invalid assertions are rejected.
func TestIntegration_Assert_InvalidAssertion(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
tests := []struct {
name string
assertion Assertion
wantErr bool
}{
{
name: "missing source_hash",
assertion: NewAssertion("Subject", "Predicate").
WithText("value").
Build(),
wantErr: true,
},
{
name: "invalid confidence",
assertion: NewAssertion("Subject", "Predicate").
WithText("value").
WithConfidence(1.5).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build(),
wantErr: true,
},
{
name: "negative confidence",
assertion: NewAssertion("Subject", "Predicate").
WithText("value").
WithConfidence(-0.1).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build(),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := client.Assert(ctx, tt.assertion)
if (err != nil) != tt.wantErr {
t.Errorf("Assert() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr && err != nil {
t.Logf("Got expected error: %v", err)
}
})
}
}
// TestIntegration_Lifecycle tests lifecycle stage filtering.
func TestIntegration_Lifecycle(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
subject := uniqueSubject("IntegrationTest_Lifecycle")
predicate := "has_status"
// Create assertions with different lifecycle stages
stages := []LifecycleStage{
LifecycleProposed,
LifecycleApproved,
LifecycleDeprecated,
}
for _, stage := range stages {
assertion := NewAssertion(subject, predicate).
WithText(string(stage)).
WithLifecycle(stage).
WithConfidence(0.9).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
hash, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert() for lifecycle %s failed: %v", stage, err)
}
t.Logf("Created assertion with lifecycle %s: %s", stage, hash)
}
// Small delay for indexing
time.Sleep(100 * time.Millisecond)
// Query for only Approved lifecycle
params := NewQuery().
WithSubject(subject).
WithPredicate(predicate).
WithLifecycle(LifecycleApproved).
Build()
result, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() failed: %v", err)
}
// Should only get the Approved assertion
for _, a := range result.Assertions {
if a.Lifecycle != LifecycleApproved {
t.Errorf("Got assertion with lifecycle %s, want Approved only", a.Lifecycle)
}
}
t.Logf("Lifecycle filtering returned %d Approved assertions", result.TotalCount)
}
// TestIntegration_Trace tests the trace endpoint for audit trail querying.
func TestIntegration_Trace(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
// Create a unique subject for this test
subject := uniqueSubject("IntegrationTest_Trace")
predicate := "has_trace_value"
// Create an assertion to generate query activity
assertion := NewAssertion(subject, predicate).
WithText("test_trace_value").
WithConfidence(0.95).
WithLifecycle(LifecycleApproved).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
hash, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert() failed: %v", err)
}
t.Logf("Created assertion with hash: %s", hash)
// Small delay for indexing
time.Sleep(100 * time.Millisecond)
// Perform a query to create audit trail
params := NewQuery().
WithSubject(subject).
WithPredicate(predicate).
Build()
queryResult, err := client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() failed: %v", err)
}
if queryResult.TotalCount == 0 {
t.Fatal("Query() returned no results")
}
t.Logf("Query returned %d results", queryResult.TotalCount)
// Small delay for audit to be written
time.Sleep(100 * time.Millisecond)
// Get the agent ID from the client's signer
agentID := client.signer.PublicKey()
// Calculate time range (last 1 hour to now)
now := time.Now().Unix()
from := now - 3600
// Trace the agent's queries
traceParams := TraceParams{
AgentID: agentID,
From: fmt.Sprintf("%d", from),
To: fmt.Sprintf("%d", now),
Subject: subject, // Filter to our test subject
Limit: 100,
}
traceResult, err := client.Trace(ctx, traceParams)
if err != nil {
t.Fatalf("Trace() failed: %v", err)
}
t.Logf("Trace returned %d audit records", traceResult.TotalCount)
// Verify we got at least one audit record
if traceResult.TotalCount == 0 {
t.Error("Trace() returned no audit records")
}
// Verify the trace result has the correct agent ID
if traceResult.AgentID != agentID {
t.Errorf("TraceResult.AgentID = %s, want %s", traceResult.AgentID, agentID)
}
// Verify the trace result has the correct time range
if traceResult.FromTimestamp != uint64(from) {
t.Errorf("TraceResult.FromTimestamp = %d, want %d", traceResult.FromTimestamp, from)
}
// Verify audit records contain our query
foundOurQuery := false
for _, audit := range traceResult.Audits {
t.Logf("Audit record: query_id=%s, timestamp=%d, subject=%v, predicate=%v",
audit.QueryID, audit.Timestamp, audit.Params.Subject, audit.Params.Predicate)
// Check if this audit is for our query
if audit.Params.Subject != nil && *audit.Params.Subject == subject &&
audit.Params.Predicate != nil && *audit.Params.Predicate == predicate {
foundOurQuery = true
// Verify agent ID
if audit.AgentID == nil || *audit.AgentID != agentID {
t.Errorf("Audit.AgentID = %v, want %s", audit.AgentID, agentID)
}
// Verify result hash is present
if audit.ResultHash == nil {
t.Error("Audit.ResultHash is nil, expected a result")
}
// Verify contributing assertions
if len(audit.ContributingAssertions) == 0 {
t.Error("Audit.ContributingAssertions is empty, expected at least one")
}
}
}
if !foundOurQuery {
t.Errorf("Did not find audit record for our query (subject=%s, predicate=%s)", subject, predicate)
}
}
// TestIntegration_Trace_WithPattern tests trace with subject pattern filtering.
func TestIntegration_Trace_WithPattern(t *testing.T) {
client := getTestClient(t)
ctx := context.Background()
// Create unique subjects with a common prefix
prefix := uniqueSubject("IntegrationTest_TracePattern")
subject1 := prefix + "_A"
subject2 := prefix + "_B"
predicate := "has_value"
// Create assertions and query them
for _, subj := range []string{subject1, subject2} {
assertion := NewAssertion(subj, predicate).
WithText("test_value").
WithConfidence(0.9).
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
Build()
_, err := client.Assert(ctx, assertion)
if err != nil {
t.Fatalf("Assert() for %s failed: %v", subj, err)
}
time.Sleep(50 * time.Millisecond)
// Query to create audit trail
params := NewQuery().WithSubject(subj).WithPredicate(predicate).Build()
_, err = client.Query(ctx, params)
if err != nil {
t.Fatalf("Query() for %s failed: %v", subj, err)
}
}
time.Sleep(100 * time.Millisecond)
// Trace with wildcard pattern
agentID := client.signer.PublicKey()
now := time.Now().Unix()
from := now - 3600
traceParams := TraceParams{
AgentID: agentID,
From: fmt.Sprintf("%d", from),
To: fmt.Sprintf("%d", now),
Subject: prefix + "*", // Match both subjects
Limit: 100,
}
traceResult, err := client.Trace(ctx, traceParams)
if err != nil {
t.Fatalf("Trace() failed: %v", err)
}
t.Logf("Trace with pattern returned %d audit records", traceResult.TotalCount)
// Should find both queries
foundSubjects := make(map[string]bool)
for _, audit := range traceResult.Audits {
if audit.Params.Subject != nil {
foundSubjects[*audit.Params.Subject] = true
}
}
if !foundSubjects[subject1] {
t.Errorf("Did not find audit for subject %s", subject1)
}
if !foundSubjects[subject2] {
t.Errorf("Did not find audit for subject %s", subject2)
}
}