- 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>
551 lines
14 KiB
Go
551 lines
14 KiB
Go
package steme
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"testing"
|
|
)
|
|
|
|
// Note: context is used in the skipped integration test
|
|
|
|
// TestSignerGeneration tests keypair generation and serialization.
|
|
func TestSignerGeneration(t *testing.T) {
|
|
signer, err := GenerateSigner()
|
|
if err != nil {
|
|
t.Fatalf("GenerateSigner() failed: %v", err)
|
|
}
|
|
|
|
// Check public key is 64 hex chars (32 bytes)
|
|
pubKey := signer.PublicKey()
|
|
if len(pubKey) != 64 {
|
|
t.Errorf("PublicKey() length = %d, want 64", len(pubKey))
|
|
}
|
|
|
|
// Check seed is 64 hex chars (32 bytes)
|
|
seed := signer.Seed()
|
|
if len(seed) != 64 {
|
|
t.Errorf("Seed() length = %d, want 64", len(seed))
|
|
}
|
|
|
|
// Check we can reconstruct signer from seed
|
|
signer2, err := NewSignerFromHex(seed)
|
|
if err != nil {
|
|
t.Fatalf("NewSignerFromHex() failed: %v", err)
|
|
}
|
|
|
|
if signer.PublicKey() != signer2.PublicKey() {
|
|
t.Errorf("PublicKey mismatch after reconstruction")
|
|
}
|
|
}
|
|
|
|
// TestSignerSignAndVerify tests signature creation and verification.
|
|
func TestSignerSignAndVerify(t *testing.T) {
|
|
signer, err := GenerateSigner()
|
|
if err != nil {
|
|
t.Fatalf("GenerateSigner() failed: %v", err)
|
|
}
|
|
|
|
message := []byte("test message")
|
|
signature := signer.Sign(message)
|
|
|
|
// Signature should be 128 hex chars (64 bytes)
|
|
if len(signature) != 128 {
|
|
t.Errorf("Sign() signature length = %d, want 128", len(signature))
|
|
}
|
|
|
|
// Verify the signature
|
|
err = Verify(signer.PublicKey(), signature, message)
|
|
if err != nil {
|
|
t.Errorf("Verify() failed: %v", err)
|
|
}
|
|
|
|
// Verify fails with wrong message
|
|
err = Verify(signer.PublicKey(), signature, []byte("wrong message"))
|
|
if err == nil {
|
|
t.Errorf("Verify() should fail with wrong message")
|
|
}
|
|
}
|
|
|
|
// TestAssertionBuilder tests the fluent assertion builder API.
|
|
func TestAssertionBuilder(t *testing.T) {
|
|
lifecycle := LifecycleApproved
|
|
sourceClass := SourceClassClinical
|
|
|
|
assertion := NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithConfidence(0.95).
|
|
WithLifecycle(lifecycle).
|
|
WithSourceClass(sourceClass).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
|
|
if assertion.Subject != "Tesla_Inc" {
|
|
t.Errorf("Subject = %s, want Tesla_Inc", assertion.Subject)
|
|
}
|
|
|
|
if assertion.Predicate != "has_revenue" {
|
|
t.Errorf("Predicate = %s, want has_revenue", assertion.Predicate)
|
|
}
|
|
|
|
if assertion.Object.Type != "Number" {
|
|
t.Errorf("Object.Type = %s, want Number", assertion.Object.Type)
|
|
}
|
|
|
|
if assertion.Object.Value != 96.7 {
|
|
t.Errorf("Object.Value = %v, want 96.7", assertion.Object.Value)
|
|
}
|
|
|
|
if assertion.Confidence != 0.95 {
|
|
t.Errorf("Confidence = %f, want 0.95", assertion.Confidence)
|
|
}
|
|
|
|
if assertion.Lifecycle == nil || *assertion.Lifecycle != LifecycleApproved {
|
|
t.Errorf("Lifecycle = %v, want Approved", assertion.Lifecycle)
|
|
}
|
|
|
|
if assertion.SourceClass == nil || *assertion.SourceClass != SourceClassClinical {
|
|
t.Errorf("SourceClass = %v, want Clinical", assertion.SourceClass)
|
|
}
|
|
}
|
|
|
|
// TestAssertionValidation tests assertion validation.
|
|
func TestAssertionValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
build func() Assertion
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid assertion",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithConfidence(0.95).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "confidence too high",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithConfidence(1.5).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "confidence negative",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithConfidence(-0.1).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing source_hash",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
Build()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid source_hash length",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithSourceHash("00").
|
|
Build()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid source_hash hex",
|
|
build: func() Assertion {
|
|
return NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithSourceHash("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").
|
|
Build()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assertion := tt.build()
|
|
err := assertion.Validate()
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestQueryBuilder tests the fluent query builder API.
|
|
func TestQueryBuilder(t *testing.T) {
|
|
lens := LensConsensus
|
|
lifecycle := LifecycleApproved
|
|
|
|
params := NewQuery().
|
|
WithSubject("Tesla_Inc").
|
|
WithPredicate("has_revenue").
|
|
WithLens(lens).
|
|
WithLifecycle(lifecycle).
|
|
WithLimit(10).
|
|
Build()
|
|
|
|
if params.Subject == nil || *params.Subject != "Tesla_Inc" {
|
|
t.Errorf("Subject = %v, want Tesla_Inc", params.Subject)
|
|
}
|
|
|
|
if params.Predicate == nil || *params.Predicate != "has_revenue" {
|
|
t.Errorf("Predicate = %v, want has_revenue", params.Predicate)
|
|
}
|
|
|
|
if params.Lens == nil || *params.Lens != LensConsensus {
|
|
t.Errorf("Lens = %v, want Consensus", params.Lens)
|
|
}
|
|
|
|
if params.Lifecycle == nil || *params.Lifecycle != LifecycleApproved {
|
|
t.Errorf("Lifecycle = %v, want Approved", params.Lifecycle)
|
|
}
|
|
|
|
if params.Limit != 10 {
|
|
t.Errorf("Limit = %d, want 10", params.Limit)
|
|
}
|
|
}
|
|
|
|
// TestCanonicalMessage tests the canonical message generation for signing.
|
|
func TestCanonicalMessage(t *testing.T) {
|
|
assertion := NewAssertion("Tesla_Inc", "has_revenue").
|
|
WithNumber(96.7).
|
|
WithConfidence(0.95).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
|
|
msg1, err := canonicalAssertionMessage(&assertion)
|
|
if err != nil {
|
|
t.Fatalf("canonicalAssertionMessage() failed: %v", err)
|
|
}
|
|
|
|
// Verify format matches server expectation: "{subject}:{predicate}"
|
|
expected := "Tesla_Inc:has_revenue"
|
|
if string(msg1) != expected {
|
|
t.Errorf("canonicalAssertionMessage() = %q, want %q", string(msg1), expected)
|
|
}
|
|
|
|
// Same assertion should produce same message
|
|
msg2, err := canonicalAssertionMessage(&assertion)
|
|
if err != nil {
|
|
t.Fatalf("canonicalAssertionMessage() failed: %v", err)
|
|
}
|
|
|
|
if string(msg1) != string(msg2) {
|
|
t.Errorf("Canonical message not deterministic")
|
|
}
|
|
|
|
// Different subject/predicate should produce different message
|
|
assertion2 := NewAssertion("Tesla_Inc", "has_employees"). // Different predicate
|
|
WithNumber(97.0).
|
|
WithConfidence(0.95).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
|
|
msg3, err := canonicalAssertionMessage(&assertion2)
|
|
if err != nil {
|
|
t.Fatalf("canonicalAssertionMessage() failed: %v", err)
|
|
}
|
|
|
|
if string(msg1) == string(msg3) {
|
|
t.Errorf("Different assertions produced same canonical message")
|
|
}
|
|
}
|
|
|
|
// TestClientIntegration is a placeholder for integration tests.
|
|
//
|
|
// This would test against a real StemeDB server. For now, it's skipped
|
|
// unless the STEMEDB_URL environment variable is set.
|
|
func TestClientIntegration(t *testing.T) {
|
|
t.Skip("Integration test requires running StemeDB server")
|
|
|
|
// Example integration test structure:
|
|
/*
|
|
baseURL := os.Getenv("STEMEDB_URL")
|
|
if baseURL == "" {
|
|
t.Skip("STEMEDB_URL not set")
|
|
}
|
|
|
|
signer, err := GenerateSigner()
|
|
if err != nil {
|
|
t.Fatalf("GenerateSigner() failed: %v", err)
|
|
}
|
|
|
|
client := NewClient(baseURL, signer)
|
|
|
|
// Test health check
|
|
health, err := client.Health(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Health() failed: %v", err)
|
|
}
|
|
|
|
if health.Status != "healthy" {
|
|
t.Errorf("Health.Status = %s, want healthy", health.Status)
|
|
}
|
|
|
|
// Test assertion creation
|
|
assertion := NewAssertion("test_entity", "test_predicate").
|
|
WithText("test value").
|
|
WithConfidence(0.95).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
|
|
hash, err := client.Assert(context.Background(), assertion)
|
|
if err != nil {
|
|
t.Fatalf("Assert() failed: %v", err)
|
|
}
|
|
|
|
if len(hash) != 64 {
|
|
t.Errorf("Assert() hash length = %d, want 64", len(hash))
|
|
}
|
|
|
|
// Test query
|
|
params := NewQuery().
|
|
WithSubject("test_entity").
|
|
WithPredicate("test_predicate").
|
|
Build()
|
|
|
|
result, err := client.Query(context.Background(), params)
|
|
if err != nil {
|
|
t.Fatalf("Query() failed: %v", err)
|
|
}
|
|
|
|
if result.TotalCount == 0 {
|
|
t.Errorf("Query() returned no results")
|
|
}
|
|
*/
|
|
}
|
|
|
|
// TestSignerFromEnvNotSet tests that SignerFromEnv fails when var is not set.
|
|
func TestSignerFromEnvNotSet(t *testing.T) {
|
|
_, err := SignerFromEnv("NONEXISTENT_STEME_VAR_12345")
|
|
if err == nil {
|
|
t.Error("SignerFromEnv() should fail when env var is not set")
|
|
}
|
|
}
|
|
|
|
// TestCanonicalMessageAllObjectTypes tests canonical message with all object types.
|
|
// The canonical message is just "{subject}:{predicate}" - object type doesn't affect it.
|
|
func TestCanonicalMessageAllObjectTypes(t *testing.T) {
|
|
types := []struct {
|
|
name string
|
|
obj ObjectValue
|
|
}{
|
|
{"text", NewTextValue("hello")},
|
|
{"number", NewNumberValue(42.5)},
|
|
{"boolean", NewBooleanValue(true)},
|
|
{"reference", NewReferenceValue("Entity_123")},
|
|
}
|
|
|
|
for _, tt := range types {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := NewAssertion("Subject", "Predicate").
|
|
WithConfidence(0.5).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
a.Object = tt.obj
|
|
|
|
msg, err := canonicalAssertionMessage(&a)
|
|
if err != nil {
|
|
t.Errorf("canonicalAssertionMessage() failed for %s: %v", tt.name, err)
|
|
}
|
|
// Canonical message is "{subject}:{predicate}" regardless of object type
|
|
expected := "Subject:Predicate"
|
|
if string(msg) != expected {
|
|
t.Errorf("Expected %q, got %q", expected, string(msg))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCanonicalMessageEdgeCases tests edge cases in canonical message generation.
|
|
func TestCanonicalMessageEdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
build func() Assertion
|
|
wantErr bool
|
|
expected string
|
|
}{
|
|
{
|
|
name: "zero confidence",
|
|
build: func() Assertion {
|
|
return NewAssertion("S", "P").
|
|
WithText("v").
|
|
WithConfidence(0.0).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: false,
|
|
expected: "S:P",
|
|
},
|
|
{
|
|
name: "max confidence",
|
|
build: func() Assertion {
|
|
return NewAssertion("S", "P").
|
|
WithText("v").
|
|
WithConfidence(1.0).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: false,
|
|
expected: "S:P",
|
|
},
|
|
{
|
|
name: "empty strings",
|
|
build: func() Assertion {
|
|
return NewAssertion("", "").
|
|
WithText("v").
|
|
WithConfidence(0.5).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: false,
|
|
expected: ":",
|
|
},
|
|
{
|
|
name: "special characters",
|
|
build: func() Assertion {
|
|
return NewAssertion("Subject:With:Colons", "Predicate").
|
|
WithText("v").
|
|
WithConfidence(0.5).
|
|
WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
|
|
Build()
|
|
},
|
|
wantErr: false,
|
|
expected: "Subject:With:Colons:Predicate",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := tt.build()
|
|
msg, err := canonicalAssertionMessage(&a)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if !tt.wantErr && string(msg) != tt.expected {
|
|
t.Errorf("got %q, want %q", string(msg), tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNewSignerInvalidSeed tests that NewSigner fails with wrong seed size.
|
|
func TestNewSignerInvalidSeed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
seedLen int
|
|
wantErr bool
|
|
}{
|
|
{"empty seed", 0, true},
|
|
{"short seed", 16, true},
|
|
{"correct seed", 32, false},
|
|
{"long seed", 64, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
seed := make([]byte, tt.seedLen)
|
|
_, err := NewSigner(seed)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVerifyInvalidInputs tests Verify with invalid inputs.
|
|
func TestVerifyInvalidInputs(t *testing.T) {
|
|
signer, _ := GenerateSigner()
|
|
message := []byte("test")
|
|
validSig := signer.Sign(message)
|
|
|
|
tests := []struct {
|
|
name string
|
|
pubKey string
|
|
sig string
|
|
wantErr bool
|
|
}{
|
|
{"valid", signer.PublicKey(), validSig, false},
|
|
{"invalid pubkey hex", "zzzz", validSig, true},
|
|
{"short pubkey", "abcd", validSig, true},
|
|
{"invalid sig hex", signer.PublicKey(), "zzzz", true},
|
|
{"short sig", signer.PublicKey(), "abcd", true},
|
|
{"wrong sig", signer.PublicKey(), hex.EncodeToString(make([]byte, 64)), true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := Verify(tt.pubKey, tt.sig, message)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestObjectValueConstructors tests the object value constructor functions.
|
|
func TestObjectValueConstructors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value ObjectValue
|
|
wantType string
|
|
wantValue any
|
|
}{
|
|
{
|
|
name: "text value",
|
|
value: NewTextValue("hello"),
|
|
wantType: "Text",
|
|
wantValue: "hello",
|
|
},
|
|
{
|
|
name: "number value",
|
|
value: NewNumberValue(42.5),
|
|
wantType: "Number",
|
|
wantValue: 42.5,
|
|
},
|
|
{
|
|
name: "boolean value",
|
|
value: NewBooleanValue(true),
|
|
wantType: "Boolean",
|
|
wantValue: true,
|
|
},
|
|
{
|
|
name: "reference value",
|
|
value: NewReferenceValue("Entity_Id"),
|
|
wantType: "Reference",
|
|
wantValue: "Entity_Id",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.value.Type != tt.wantType {
|
|
t.Errorf("Type = %s, want %s", tt.value.Type, tt.wantType)
|
|
}
|
|
|
|
if tt.value.Value != tt.wantValue {
|
|
t.Errorf("Value = %v, want %v", tt.value.Value, tt.wantValue)
|
|
}
|
|
})
|
|
}
|
|
}
|