gomcp/internal/domain/memory/genome_bootstrap_test.go

340 lines
9.1 KiB
Go

package memory
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Hardcoded Genes Tests ---
func TestHardcodedGenes_CountAndIDs(t *testing.T) {
require.Len(t, HardcodedGenes, 6, "Must have exactly 6 hardcoded genes")
expectedIDs := []string{
"GENE_01_SOVEREIGNTY",
"GENE_02_PERSISTENCE",
"GENE_03_STEALTH_MIMICRY",
"GENE_04_SENTINEL_PROTECTION",
"GENE_05_RESONANCE",
"GENE_06_EXTRACTION",
}
for i, g := range HardcodedGenes {
assert.Equal(t, expectedIDs[i], g.ID, "Gene %d ID mismatch", i)
assert.NotEmpty(t, g.Content, "Gene %s must have content", g.ID)
assert.NotEmpty(t, g.Domain, "Gene %s must have domain", g.ID)
}
}
func TestCompiledGenomeHash_Deterministic(t *testing.T) {
h1 := CompiledGenomeHash()
h2 := CompiledGenomeHash()
assert.Equal(t, h1, h2, "CompiledGenomeHash must be deterministic")
assert.Len(t, h1, 64, "Must be SHA-256 hex string (64 chars)")
}
func TestCompiledGenomeHash_ChangesOnMutation(t *testing.T) {
original := CompiledGenomeHash()
assert.NotEmpty(t, original)
// Hash is computed from HardcodedGenes which is a package-level var.
// We cannot mutate it in a test without breaking other tests,
// but we can verify the hash is non-zero and consistent.
assert.Len(t, original, 64)
}
// --- Gene Immutability Tests ---
func TestGene_IsImmutable(t *testing.T) {
gene := NewGene("test survival invariant", "test")
assert.True(t, gene.IsGene, "Gene must have IsGene=true")
assert.True(t, gene.IsImmutable(), "Gene must be immutable")
assert.Equal(t, LevelProject, gene.Level, "Gene must be L0 (project)")
assert.Equal(t, "genome", gene.Source, "Gene source must be 'genome'")
}
func TestGene_CannotBeFact(t *testing.T) {
// Regular fact is NOT a gene.
fact := NewFact("some fact", LevelModule, "test", "mod")
assert.False(t, fact.IsGene)
assert.False(t, fact.IsImmutable())
}
// --- External Genome Tests ---
func TestLoadExternalGenome_NoFile(t *testing.T) {
genes, trusted := LoadExternalGenome("/nonexistent/path/genome.json")
assert.Nil(t, genes)
assert.False(t, trusted)
}
func TestLoadExternalGenome_ValidHash(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "genome.json")
// Write a valid genome.json with correct hash.
cfg := ExternalGenomeConfig{
Version: "1.0",
Hash: CompiledGenomeHash(),
Genes: []GeneDef{
{ID: "GENE_EXTERNAL_01", Content: "External gene for testing", Domain: "test"},
},
}
data, err := json.MarshalIndent(cfg, "", " ")
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0o644))
genes, trusted := LoadExternalGenome(path)
assert.True(t, trusted, "Valid hash must be trusted")
assert.Len(t, genes, 1)
assert.Equal(t, "GENE_EXTERNAL_01", genes[0].ID)
}
func TestLoadExternalGenome_TamperedHash(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "genome.json")
// Write genome.json with WRONG hash (tamper simulation).
cfg := ExternalGenomeConfig{
Version: "1.0",
Hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
Genes: []GeneDef{
{ID: "GENE_EVIL", Content: "Malicious gene injected by adversary", Domain: "evil"},
},
}
data, err := json.MarshalIndent(cfg, "", " ")
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0o644))
genes, trusted := LoadExternalGenome(path)
assert.Nil(t, genes, "Tampered genes must be rejected")
assert.False(t, trusted, "Tampered hash must not be trusted")
}
func TestLoadExternalGenome_InvalidJSON(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "genome.json")
require.NoError(t, os.WriteFile(path, []byte("not json"), 0o644))
genes, trusted := LoadExternalGenome(path)
assert.Nil(t, genes)
assert.False(t, trusted)
}
// --- WriteGenomeJSON Tests ---
func TestWriteGenomeJSON_RoundTrip(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "genome.json")
require.NoError(t, WriteGenomeJSON(path))
// Read it back.
data, err := os.ReadFile(path)
require.NoError(t, err)
var cfg ExternalGenomeConfig
require.NoError(t, json.Unmarshal(data, &cfg))
assert.Equal(t, "1.0", cfg.Version)
assert.Equal(t, CompiledGenomeHash(), cfg.Hash)
assert.Len(t, cfg.Genes, len(HardcodedGenes))
}
// --- BootstrapGenome Tests ---
func TestBootstrapGenome_WithInMemoryStore(t *testing.T) {
store := newInMemoryFactStore()
ctx := context.Background()
// First bootstrap — should add all 4 genes.
count, err := BootstrapGenome(ctx, store, "/nonexistent/genome.json")
require.NoError(t, err)
assert.Equal(t, 6, count, "Must bootstrap exactly 6 genes")
// Second bootstrap — idempotent, should add 0.
count2, err := BootstrapGenome(ctx, store, "/nonexistent/genome.json")
require.NoError(t, err)
assert.Equal(t, 0, count2, "Second bootstrap must be idempotent (0 new)")
// Verify all genes are present and immutable.
genes, err := store.ListGenes(ctx)
require.NoError(t, err)
assert.Len(t, genes, 6)
for _, g := range genes {
assert.True(t, g.IsGene, "Gene %s must have IsGene=true", g.ID)
assert.True(t, g.IsImmutable(), "Gene %s must be immutable", g.ID)
assert.Equal(t, LevelProject, g.Level, "Gene %s must be L0", g.ID)
}
}
func TestBootstrapGenome_WithExternalGenes(t *testing.T) {
store := newInMemoryFactStore()
ctx := context.Background()
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "genome.json")
// Write valid external genome with extra gene.
cfg := ExternalGenomeConfig{
Version: "1.0",
Hash: CompiledGenomeHash(),
Genes: []GeneDef{
{ID: "GENE_EXTERNAL_EXTRA", Content: "Extra external gene", Domain: "external"},
},
}
data, _ := json.MarshalIndent(cfg, "", " ")
require.NoError(t, os.WriteFile(path, data, 0o644))
count, err := BootstrapGenome(ctx, store, path)
require.NoError(t, err)
assert.Equal(t, 7, count, "6 hardcoded + 1 external = 7")
genes, err := store.ListGenes(ctx)
require.NoError(t, err)
assert.Len(t, genes, 7)
}
// --- In-memory FactStore for testing ---
type inMemoryFactStore struct {
facts map[string]*Fact
}
func newInMemoryFactStore() *inMemoryFactStore {
return &inMemoryFactStore{facts: make(map[string]*Fact)}
}
func (s *inMemoryFactStore) Add(_ context.Context, fact *Fact) error {
if _, exists := s.facts[fact.ID]; exists {
return fmt.Errorf("UNIQUE constraint: fact %s already exists", fact.ID)
}
f := *fact
s.facts[fact.ID] = &f
return nil
}
func (s *inMemoryFactStore) Get(_ context.Context, id string) (*Fact, error) {
f, ok := s.facts[id]
if !ok {
return nil, fmt.Errorf("fact %s not found", id)
}
return f, nil
}
func (s *inMemoryFactStore) Update(_ context.Context, fact *Fact) error {
if fact.IsGene {
return ErrImmutableFact
}
s.facts[fact.ID] = fact
return nil
}
func (s *inMemoryFactStore) Delete(_ context.Context, id string) error {
f, ok := s.facts[id]
if !ok {
return fmt.Errorf("fact %s not found", id)
}
if f.IsGene {
return ErrImmutableFact
}
delete(s.facts, id)
return nil
}
func (s *inMemoryFactStore) ListByDomain(_ context.Context, domain string, includeStale bool) ([]*Fact, error) {
var result []*Fact
for _, f := range s.facts {
if f.Domain == domain && (includeStale || !f.IsStale) {
result = append(result, f)
}
}
return result, nil
}
func (s *inMemoryFactStore) ListByLevel(_ context.Context, level HierLevel) ([]*Fact, error) {
var result []*Fact
for _, f := range s.facts {
if f.Level == level {
result = append(result, f)
}
}
return result, nil
}
func (s *inMemoryFactStore) ListDomains(_ context.Context) ([]string, error) {
domains := make(map[string]bool)
for _, f := range s.facts {
if f.Domain != "" {
domains[f.Domain] = true
}
}
var result []string
for d := range domains {
result = append(result, d)
}
return result, nil
}
func (s *inMemoryFactStore) GetStale(_ context.Context, includeArchived bool) ([]*Fact, error) {
var result []*Fact
for _, f := range s.facts {
if f.IsStale && (includeArchived || !f.IsArchived) {
result = append(result, f)
}
}
return result, nil
}
func (s *inMemoryFactStore) Search(_ context.Context, _ string, _ int) ([]*Fact, error) {
return nil, nil
}
func (s *inMemoryFactStore) ListGenes(_ context.Context) ([]*Fact, error) {
var result []*Fact
for _, f := range s.facts {
if f.IsGene {
result = append(result, f)
}
}
return result, nil
}
func (s *inMemoryFactStore) GetExpired(_ context.Context) ([]*Fact, error) {
return nil, nil
}
func (s *inMemoryFactStore) RefreshTTL(_ context.Context, _ string) error {
return nil
}
func (s *inMemoryFactStore) TouchFact(_ context.Context, _ string) error { return nil }
func (s *inMemoryFactStore) GetColdFacts(_ context.Context, _ int) ([]*Fact, error) {
return nil, nil
}
func (s *inMemoryFactStore) CompressFacts(_ context.Context, _ []string, _ string) (string, error) {
return "", nil
}
func (s *inMemoryFactStore) Stats(_ context.Context) (*FactStoreStats, error) {
stats := &FactStoreStats{
TotalFacts: len(s.facts),
ByLevel: make(map[HierLevel]int),
ByDomain: make(map[string]int),
}
for _, f := range s.facts {
stats.ByLevel[f.Level]++
if f.Domain != "" {
stats.ByDomain[f.Domain]++
}
if f.IsGene {
stats.GeneCount++
}
}
return stats, nil
}