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>
715 lines
18 KiB
Go
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)
|
|
}
|
|
}
|