//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) } }