stemedb/sdk/go/steme/steme_test.go
jordan c59066949a feat: Add quickstart "Beyond Hello World" sections with Skeptic and Layered endpoints
- 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>
2026-02-01 21:00:59 -07:00

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