- Add Layered() method to Go SDK for per-source-class consensus queries - Add LayeredQueryParams, LayeredResult, TierResolution types to Go SDK - Create conflict example demonstrating Skeptic and Layered endpoints - Update quickstart.md with sections 6 (conflict detection) and 7 (authority tiers) - Remove tracked Go binary and add data/ to .gitignore The new quickstart sections demonstrate Episteme's differentiating features: - Skeptic endpoint shows "Trust but Verify" conflict analysis - Layered endpoint shows per-tier resolution (Clinical vs Anecdotal) Note: Pre-existing large files flagged by pre-commit hook (technical debt from prior sessions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
619 lines
16 KiB
Go
619 lines
16 KiB
Go
package adk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/orchard9/stemedb-go/steme"
|
|
)
|
|
|
|
// mockClient implements EpistemeClient for testing
|
|
type mockClient struct {
|
|
queryFunc func(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error)
|
|
assertFunc func(ctx context.Context, assertion steme.Assertion) (string, error)
|
|
traceFunc func(ctx context.Context, params steme.TraceParams) (*steme.TraceResult, error)
|
|
supersedeFunc func(ctx context.Context, params steme.SupersedeParams) (*steme.SupersedeResult, error)
|
|
}
|
|
|
|
func (m *mockClient) Query(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error) {
|
|
if m.queryFunc != nil {
|
|
return m.queryFunc(ctx, params)
|
|
}
|
|
return &steme.QueryResult{}, nil
|
|
}
|
|
|
|
func (m *mockClient) Assert(ctx context.Context, assertion steme.Assertion) (string, error) {
|
|
if m.assertFunc != nil {
|
|
return m.assertFunc(ctx, assertion)
|
|
}
|
|
return "mock_hash", nil
|
|
}
|
|
|
|
func (m *mockClient) Trace(ctx context.Context, params steme.TraceParams) (*steme.TraceResult, error) {
|
|
if m.traceFunc != nil {
|
|
return m.traceFunc(ctx, params)
|
|
}
|
|
return &steme.TraceResult{}, nil
|
|
}
|
|
|
|
func (m *mockClient) Supersede(ctx context.Context, params steme.SupersedeParams) (*steme.SupersedeResult, error) {
|
|
if m.supersedeFunc != nil {
|
|
return m.supersedeFunc(ctx, params)
|
|
}
|
|
return &steme.SupersedeResult{
|
|
Status: "superseded",
|
|
TargetHash: params.TargetHash,
|
|
SupersessionType: params.SupersessionType,
|
|
Timestamp: 1704067200,
|
|
}, nil
|
|
}
|
|
|
|
func TestQueryTool(t *testing.T) {
|
|
// Mock client that returns a test assertion
|
|
client := &mockClient{
|
|
queryFunc: func(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error) {
|
|
lifecycle := steme.LifecycleApproved
|
|
return &steme.QueryResult{
|
|
Assertions: []steme.AssertionResponse{
|
|
{
|
|
Hash: "test_hash",
|
|
Subject: "Tesla_Inc",
|
|
Predicate: "has_revenue",
|
|
Object: steme.NewNumberValue(96.7),
|
|
Confidence: 0.95,
|
|
SourceHash: "source_hash",
|
|
Lifecycle: lifecycle,
|
|
},
|
|
},
|
|
TotalCount: 1,
|
|
HasMore: false,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
tool := NewQueryTool(client)
|
|
|
|
// Test basic query
|
|
input := QueryInput{
|
|
Subject: "Tesla_Inc",
|
|
Predicate: "has_revenue",
|
|
Lens: "consensus",
|
|
}
|
|
|
|
inputBytes, err := json.Marshal(input)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal input: %v", err)
|
|
}
|
|
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("query failed: %v", err)
|
|
}
|
|
|
|
var output QueryOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Verify output
|
|
if output.Confidence != 0.95 {
|
|
t.Errorf("expected confidence 0.95, got %f", output.Confidence)
|
|
}
|
|
|
|
if output.Lifecycle != "Approved" {
|
|
t.Errorf("expected lifecycle Approved, got %s", output.Lifecycle)
|
|
}
|
|
|
|
if output.QueryID == "" {
|
|
t.Error("expected non-empty query ID")
|
|
}
|
|
|
|
if len(output.Sources) == 0 {
|
|
t.Error("expected at least one source")
|
|
}
|
|
}
|
|
|
|
func TestQueryToolConfidenceThreshold(t *testing.T) {
|
|
// Mock client that returns low confidence
|
|
client := &mockClient{
|
|
queryFunc: func(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error) {
|
|
lifecycle := steme.LifecycleApproved
|
|
return &steme.QueryResult{
|
|
Assertions: []steme.AssertionResponse{
|
|
{
|
|
Hash: "test_hash",
|
|
Subject: "test",
|
|
Predicate: "test",
|
|
Object: steme.NewTextValue("test"),
|
|
Confidence: 0.6, // Low confidence
|
|
SourceHash: "source_hash",
|
|
Lifecycle: lifecycle,
|
|
},
|
|
},
|
|
TotalCount: 1,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
tool := NewQueryTool(client)
|
|
|
|
input := QueryInput{
|
|
Subject: "test",
|
|
Predicate: "test",
|
|
MinConfidence: 0.8, // Require high confidence
|
|
}
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("query failed: %v", err)
|
|
}
|
|
|
|
var output QueryOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Should have error about low confidence
|
|
if output.Error == "" {
|
|
t.Error("expected error for low confidence")
|
|
}
|
|
}
|
|
|
|
func TestAssertTool(t *testing.T) {
|
|
// Track what assertion was created
|
|
var capturedAssertion steme.Assertion
|
|
|
|
client := &mockClient{
|
|
assertFunc: func(ctx context.Context, assertion steme.Assertion) (string, error) {
|
|
capturedAssertion = assertion
|
|
return "created_hash", nil
|
|
},
|
|
}
|
|
|
|
tool := NewAssertTool(client)
|
|
|
|
input := AssertInput{
|
|
Subject: "Tesla_Inc",
|
|
Predicate: "has_revenue",
|
|
Object: 96.7,
|
|
SourceHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
Confidence: 0.95,
|
|
Lifecycle: "approved",
|
|
}
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("assert failed: %v", err)
|
|
}
|
|
|
|
var output AssertOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Verify output
|
|
if !output.Success {
|
|
t.Errorf("expected success, got error: %s", output.Error)
|
|
}
|
|
|
|
if output.Hash != "created_hash" {
|
|
t.Errorf("expected hash created_hash, got %s", output.Hash)
|
|
}
|
|
|
|
// Verify captured assertion
|
|
if capturedAssertion.Subject != "Tesla_Inc" {
|
|
t.Errorf("expected subject Tesla_Inc, got %s", capturedAssertion.Subject)
|
|
}
|
|
|
|
// Use approximate comparison for floating point
|
|
if capturedAssertion.Confidence < 0.94 || capturedAssertion.Confidence > 0.96 {
|
|
t.Errorf("expected confidence ~0.95, got %f", capturedAssertion.Confidence)
|
|
}
|
|
}
|
|
|
|
func TestConstraintCheckTool(t *testing.T) {
|
|
// Mock client that returns constraint assertions
|
|
client := &mockClient{
|
|
queryFunc: func(ctx context.Context, params steme.QueryParams) (*steme.QueryResult, error) {
|
|
lifecycle := steme.LifecycleApproved
|
|
return &steme.QueryResult{
|
|
Assertions: []steme.AssertionResponse{
|
|
{
|
|
Hash: "constraint1",
|
|
Subject: "auth_jwt",
|
|
Predicate: "must_use",
|
|
Object: steme.NewTextValue("Ed25519"),
|
|
Confidence: 1.0,
|
|
SourceHash: "source",
|
|
Lifecycle: lifecycle,
|
|
},
|
|
{
|
|
Hash: "constraint2",
|
|
Subject: "auth_jwt",
|
|
Predicate: "forbidden",
|
|
Object: steme.NewTextValue("MD5"),
|
|
Confidence: 1.0,
|
|
SourceHash: "source",
|
|
Lifecycle: lifecycle,
|
|
},
|
|
},
|
|
TotalCount: 2,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
tool := NewConstraintCheckTool(client)
|
|
|
|
input := ConstraintCheckInput{
|
|
Context: "auth_jwt",
|
|
}
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("constraint check failed: %v", err)
|
|
}
|
|
|
|
var output ConstraintCheckOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Verify constraints were returned
|
|
if len(output.Constraints) != 2 {
|
|
t.Errorf("expected 2 constraints, got %d", len(output.Constraints))
|
|
}
|
|
|
|
// Check for must-use constraint
|
|
foundMustUse := false
|
|
for _, c := range output.Constraints {
|
|
if c.MustUse == "Ed25519" {
|
|
foundMustUse = true
|
|
}
|
|
}
|
|
if !foundMustUse {
|
|
t.Error("expected to find Ed25519 must-use constraint")
|
|
}
|
|
|
|
// Check for forbidden constraint
|
|
foundForbidden := false
|
|
for _, c := range output.Constraints {
|
|
if c.Forbidden == "MD5" {
|
|
foundForbidden = true
|
|
}
|
|
}
|
|
if !foundForbidden {
|
|
t.Error("expected to find MD5 forbidden constraint")
|
|
}
|
|
}
|
|
|
|
func TestTraceTool(t *testing.T) {
|
|
// Mock client that returns audit records
|
|
agentID := "deadbeef00000000000000000000000000000000000000000000000000000000"
|
|
subject := "Tesla_Inc"
|
|
predicate := "has_revenue"
|
|
resultHash := "result_hash_123"
|
|
lens := steme.LensConsensus
|
|
|
|
client := &mockClient{
|
|
traceFunc: func(ctx context.Context, params steme.TraceParams) (*steme.TraceResult, error) {
|
|
return &steme.TraceResult{
|
|
Audits: []steme.QueryAuditRecord{
|
|
{
|
|
QueryID: "query_123",
|
|
AgentID: &agentID,
|
|
Timestamp: 1704067200,
|
|
Params: steme.QueryParamsAudit{
|
|
Subject: &subject,
|
|
Predicate: &predicate,
|
|
Lens: &lens,
|
|
},
|
|
ResultHash: &resultHash,
|
|
ResultConfidence: 0.95,
|
|
ContributingAssertions: []steme.ContributingAssertion{
|
|
{
|
|
AssertionHash: "assertion_1",
|
|
Weight: 1.0,
|
|
SourceHash: "source_1",
|
|
Lifecycle: steme.LifecycleApproved,
|
|
},
|
|
{
|
|
AssertionHash: "assertion_2",
|
|
Weight: 0.5,
|
|
SourceHash: "source_2",
|
|
Lifecycle: steme.LifecycleProposed,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TotalCount: 1,
|
|
AgentID: agentID,
|
|
FromTimestamp: 1704067200,
|
|
ToTimestamp: 1704153600,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
tool := NewTraceTool(client)
|
|
|
|
input := TraceInput{
|
|
AgentID: agentID,
|
|
From: "1704067200",
|
|
To: "1704153600",
|
|
Subject: "Tesla*",
|
|
}
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("trace failed: %v", err)
|
|
}
|
|
|
|
var output TraceOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Verify output
|
|
if len(output.Queries) != 1 {
|
|
t.Errorf("expected 1 query, got %d", len(output.Queries))
|
|
}
|
|
|
|
if len(output.Queries) > 0 {
|
|
query := output.Queries[0]
|
|
|
|
if query.QueryID != "query_123" {
|
|
t.Errorf("expected query_id query_123, got %s", query.QueryID)
|
|
}
|
|
|
|
if query.Subject != "Tesla_Inc" {
|
|
t.Errorf("expected subject Tesla_Inc, got %s", query.Subject)
|
|
}
|
|
|
|
if query.Predicate != "has_revenue" {
|
|
t.Errorf("expected predicate has_revenue, got %s", query.Predicate)
|
|
}
|
|
|
|
if query.Lens != "consensus" {
|
|
t.Errorf("expected lens consensus, got %s", query.Lens)
|
|
}
|
|
|
|
if query.Confidence != 0.95 {
|
|
t.Errorf("expected confidence 0.95, got %f", query.Confidence)
|
|
}
|
|
|
|
if len(query.Contributing) != 2 {
|
|
t.Errorf("expected 2 contributing assertions, got %d", len(query.Contributing))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSupersedeTool(t *testing.T) {
|
|
// Track what supersession was created
|
|
var capturedParams steme.SupersedeParams
|
|
|
|
client := &mockClient{
|
|
supersedeFunc: func(ctx context.Context, params steme.SupersedeParams) (*steme.SupersedeResult, error) {
|
|
capturedParams = params
|
|
return &steme.SupersedeResult{
|
|
Status: "superseded",
|
|
TargetHash: params.TargetHash,
|
|
SupersessionType: params.SupersessionType,
|
|
Timestamp: 1704067200,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
tool := NewSupersedeTool(client)
|
|
|
|
input := SupersedeInput{
|
|
Hash: "abc123def456",
|
|
Type: "Invalidate",
|
|
Reason: "Proposal treated as approved. See incident INC-2024-001",
|
|
NewHash: "def456abc123",
|
|
AgentID: "deadbeef00000000000000000000000000000000000000000000000000000000",
|
|
Signature: "0000000000000000000000000000000000000000000000000000000000000000" +
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
}
|
|
|
|
inputBytes, err := json.Marshal(input)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal input: %v", err)
|
|
}
|
|
|
|
outputBytes, err := tool.Execute(context.Background(), inputBytes)
|
|
if err != nil {
|
|
t.Fatalf("supersede failed: %v", err)
|
|
}
|
|
|
|
var output SupersedeOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
// Verify output
|
|
if !output.Success {
|
|
t.Errorf("expected success, got error: %s", output.Error)
|
|
}
|
|
|
|
if output.TargetHash != "abc123def456" {
|
|
t.Errorf("expected target hash abc123def456, got %s", output.TargetHash)
|
|
}
|
|
|
|
if output.SupersessionType != "Invalidate" {
|
|
t.Errorf("expected supersession type Invalidate, got %s", output.SupersessionType)
|
|
}
|
|
|
|
// Verify captured params
|
|
if capturedParams.TargetHash != "abc123def456" {
|
|
t.Errorf("expected target hash abc123def456, got %s", capturedParams.TargetHash)
|
|
}
|
|
|
|
if capturedParams.SupersessionType != steme.SupersessionInvalidate {
|
|
t.Errorf("expected supersession type Invalidate, got %s", capturedParams.SupersessionType)
|
|
}
|
|
|
|
if capturedParams.Reason != "Proposal treated as approved. See incident INC-2024-001" {
|
|
t.Errorf("expected reason about proposal, got %s", capturedParams.Reason)
|
|
}
|
|
}
|
|
|
|
func TestParseSupersessionType(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected steme.SupersessionType
|
|
}{
|
|
{"Invalidate", steme.SupersessionInvalidate},
|
|
{"invalidate", steme.SupersessionInvalidate},
|
|
{"Temporal", steme.SupersessionTemporal},
|
|
{"temporal", steme.SupersessionTemporal},
|
|
{"Refinement", steme.SupersessionRefinement},
|
|
{"RequiresReview", steme.SupersessionRequiresReview},
|
|
{"requires_review", steme.SupersessionRequiresReview},
|
|
{"Additive", steme.SupersessionAdditive},
|
|
{"additive", steme.SupersessionAdditive},
|
|
{"unknown", steme.SupersessionInvalidate}, // Default
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := parseSupersessionType(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("parseSupersessionType(%s) = %v, expected %v", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToolNames(t *testing.T) {
|
|
client := &mockClient{}
|
|
|
|
tests := []struct {
|
|
tool Tool
|
|
expectedName string
|
|
}{
|
|
{NewQueryTool(client), "episteme_query"},
|
|
{NewAssertTool(client), "episteme_assert"},
|
|
{NewConstraintCheckTool(client), "episteme_constraint_check"},
|
|
{NewTraceTool(client), "episteme_trace"},
|
|
{NewSupersedeTool(client), "episteme_supersede"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if tt.tool.Name() != tt.expectedName {
|
|
t.Errorf("expected tool name %s, got %s", tt.expectedName, tt.tool.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAllTools(t *testing.T) {
|
|
client := &mockClient{}
|
|
tools := AllTools(client)
|
|
|
|
if len(tools) != 5 {
|
|
t.Errorf("expected 5 tools, got %d", len(tools))
|
|
}
|
|
|
|
// Verify all expected tools are present
|
|
expectedNames := map[string]bool{
|
|
"episteme_query": false,
|
|
"episteme_assert": false,
|
|
"episteme_constraint_check": false,
|
|
"episteme_trace": false,
|
|
"episteme_supersede": false,
|
|
}
|
|
|
|
for _, tool := range tools {
|
|
expectedNames[tool.Name()] = true
|
|
}
|
|
|
|
for name, found := range expectedNames {
|
|
if !found {
|
|
t.Errorf("expected to find tool %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigCreation(t *testing.T) {
|
|
client := &mockClient{}
|
|
logFunc := func(format string, args ...any) {}
|
|
setState := func(key string, value any) {}
|
|
|
|
configs := AllConfigs(client, 0.8, setState, logFunc)
|
|
|
|
expectedAgents := []string{
|
|
"implementation",
|
|
"lead",
|
|
"research",
|
|
"supervisor",
|
|
"oncall",
|
|
}
|
|
|
|
for _, agentType := range expectedAgents {
|
|
config, ok := configs[agentType]
|
|
if !ok {
|
|
t.Errorf("expected config for %s", agentType)
|
|
continue
|
|
}
|
|
|
|
if config.Name == "" {
|
|
t.Errorf("config for %s has empty name", agentType)
|
|
}
|
|
|
|
if config.Description == "" {
|
|
t.Errorf("config for %s has empty description", agentType)
|
|
}
|
|
|
|
if config.Instruction == "" {
|
|
t.Errorf("config for %s has empty instruction", agentType)
|
|
}
|
|
|
|
if len(config.Tools) == 0 {
|
|
t.Errorf("config for %s has no tools", agentType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseLifecycle(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected steme.LifecycleStage
|
|
}{
|
|
{"proposed", steme.LifecycleProposed},
|
|
{"under_review", steme.LifecycleUnderReview},
|
|
{"approved", steme.LifecycleApproved},
|
|
{"deprecated", steme.LifecycleDeprecated},
|
|
{"rejected", steme.LifecycleRejected},
|
|
{"invalid", steme.LifecycleProposed}, // Default
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := parseLifecycle(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("parseLifecycle(%s) = %v, expected %v", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseLens(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected steme.Lens
|
|
}{
|
|
{"recency", steme.LensRecency},
|
|
{"consensus", steme.LensConsensus},
|
|
{"authority", steme.LensAuthority},
|
|
{"vote_aware_consensus", steme.LensVoteAwareConsensus},
|
|
{"trust_aware_authority", steme.LensTrustAwareAuthority},
|
|
{"invalid", steme.LensConsensus}, // Default
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := parseLen(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("parseLen(%s) = %v, expected %v", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|