package fn_operations import ( "os" "path/filepath" "testing" ) func tempDB(t *testing.T) *DB { t.Helper() path := filepath.Join(t.TempDir(), "test.db") db, err := Open(path) if err != nil { t.Fatalf("opening test db: %v", err) } t.Cleanup(func() { db.Close() }) return db } func TestOpenAndClose(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") db, err := Open(path) if err != nil { t.Fatalf("open: %v", err) } if err := db.Close(); err != nil { t.Fatalf("close: %v", err) } if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatal("db file should exist") } } func TestTypeSnapshotCRUD(t *testing.T) { db := tempDB(t) ts := &TypeSnapshot{ ID: "ohlcv_go_finance", Version: "1.0.0", Lang: "go", Algebraic: "product", Definition: "type OHLCV struct { ... }", Description: "Vela de mercado", } if err := db.InsertTypeSnapshot(ts); err != nil { t.Fatalf("insert: %v", err) } got, err := db.GetTypeSnapshot("ohlcv_go_finance") if err != nil { t.Fatalf("get: %v", err) } if got == nil { t.Fatal("expected snapshot, got nil") } if got.Definition != ts.Definition { t.Errorf("definition = %q, want %q", got.Definition, ts.Definition) } // INSERT OR IGNORE: second insert should not overwrite ts2 := &TypeSnapshot{ ID: "ohlcv_go_finance", Version: "2.0.0", Lang: "go", Algebraic: "product", Definition: "type OHLCV struct { CHANGED }", Description: "Changed", } if err := db.InsertTypeSnapshot(ts2); err != nil { t.Fatalf("insert duplicate: %v", err) } got2, _ := db.GetTypeSnapshot("ohlcv_go_finance") if got2.Version != "1.0.0" { t.Errorf("snapshot should be immutable, got version %q", got2.Version) } // Not found missing, err := db.GetTypeSnapshot("nonexistent") if err != nil { t.Fatalf("get missing: %v", err) } if missing != nil { t.Error("expected nil for missing snapshot") } all, err := db.ListTypeSnapshots() if err != nil { t.Fatalf("list: %v", err) } if len(all) != 1 { t.Errorf("expected 1 snapshot, got %d", len(all)) } } func TestEntityCRUD(t *testing.T) { db := tempDB(t) // Insert snapshot first (type_ref) db.InsertTypeSnapshot(&TypeSnapshot{ ID: "tick_go_finance", Version: "1.0.0", Lang: "go", Algebraic: "product", }) e := &Entity{ ID: "ticks_btcusdt_2024", Name: "ticks_btcusdt_2024", TypeRef: "tick_go_finance", Status: StatusActive, Source: "binance_api", Domain: "market_data", Tags: []string{"btc", "binance"}, Metadata: map[string]any{ "pair": "BTCUSDT", "exchange": "binance", }, } if err := db.InsertEntity(e); err != nil { t.Fatalf("insert: %v", err) } got, err := db.GetEntity("ticks_btcusdt_2024") if err != nil { t.Fatalf("get: %v", err) } if got == nil { t.Fatal("expected entity, got nil") } if got.Source != "binance_api" { t.Errorf("source = %q, want binance_api", got.Source) } if len(got.Tags) != 2 { t.Errorf("tags len = %d, want 2", len(got.Tags)) } if got.Metadata["pair"] != "BTCUSDT" { t.Errorf("metadata pair = %v, want BTCUSDT", got.Metadata["pair"]) } // Update got.Status = StatusStale if err := db.UpdateEntity(got); err != nil { t.Fatalf("update: %v", err) } updated, _ := db.GetEntity("ticks_btcusdt_2024") if updated.Status != StatusStale { t.Errorf("status = %q, want stale", updated.Status) } // List all, err := db.ListEntities("market_data", "") if err != nil { t.Fatalf("list: %v", err) } if len(all) != 1 { t.Errorf("expected 1, got %d", len(all)) } // Search found, err := db.SearchEntities("btcusdt", "") if err != nil { t.Fatalf("search: %v", err) } if len(found) != 1 { t.Errorf("search expected 1, got %d", len(found)) } // Delete if err := db.DeleteEntity("ticks_btcusdt_2024"); err != nil { t.Fatalf("delete: %v", err) } deleted, _ := db.GetEntity("ticks_btcusdt_2024") if deleted != nil { t.Error("expected nil after delete") } } func TestRelationCRUD(t *testing.T) { db := tempDB(t) // Setup entities db.InsertTypeSnapshot(&TypeSnapshot{ID: "t1", Version: "1.0.0", Lang: "go", Algebraic: "product"}) db.InsertEntity(&Entity{ID: "a", Name: "a", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "b", Name: "b", TypeRef: "t1", Status: StatusActive, Source: "test"}) r := &Relation{ ID: "a__to__b__via__transform", Name: "TRANSFORMA", FromEntity: "a", ToEntity: "b", Direction: DirUnidirectional, Status: RelDesigned, } if err := InsertRelationSafe(db, r); err != nil { t.Fatalf("insert relation: %v", err) } got, err := db.GetRelation("a__to__b__via__transform") if err != nil { t.Fatalf("get: %v", err) } if got == nil { t.Fatal("expected relation, got nil") } if got.Name != "TRANSFORMA" { t.Errorf("name = %q, want TRANSFORMA", got.Name) } // List by entity rels, err := db.ListRelations("a") if err != nil { t.Fatalf("list: %v", err) } if len(rels) != 1 { t.Errorf("expected 1, got %d", len(rels)) } // Delete if err := db.DeleteRelation("a__to__b__via__transform"); err != nil { t.Fatalf("delete: %v", err) } deleted, _ := db.GetRelation("a__to__b__via__transform") if deleted != nil { t.Error("expected nil after delete") } } func TestRelationInputs(t *testing.T) { db := tempDB(t) db.InsertTypeSnapshot(&TypeSnapshot{ID: "t1", Version: "1.0.0", Lang: "go", Algebraic: "product"}) db.InsertEntity(&Entity{ID: "a", Name: "a", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "b", Name: "b", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "c", Name: "c", TypeRef: "t1", Status: StatusActive, Source: "test"}) r := &Relation{ ID: "multi__to__c", Name: "ENRIQUECE", ToEntity: "c", Direction: DirUnidirectional, Status: RelDesigned, } inputs := []RelationInput{ {ID: "i1", RelationID: "multi__to__c", EntityID: "a", Role: "base"}, {ID: "i2", RelationID: "multi__to__c", EntityID: "b", Role: "lookup"}, } if err := InsertRelationWithInputs(db, r, inputs); err != nil { t.Fatalf("insert with inputs: %v", err) } got, err := db.GetRelationInputs("multi__to__c") if err != nil { t.Fatalf("get inputs: %v", err) } if len(got) != 2 { t.Errorf("expected 2 inputs, got %d", len(got)) } } func TestCycleDetectionCausal(t *testing.T) { db := tempDB(t) db.InsertTypeSnapshot(&TypeSnapshot{ID: "t1", Version: "1.0.0", Lang: "go", Algebraic: "product"}) db.InsertEntity(&Entity{ID: "a", Name: "a", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "b", Name: "b", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "c", Name: "c", TypeRef: "t1", Status: StatusActive, Source: "test"}) // a -> b (causal) InsertRelationSafe(db, &Relation{ ID: "ab", Name: "T1", FromEntity: "a", ToEntity: "b", Via: "fn1", Purity: "impure", Direction: DirUnidirectional, Status: RelDesigned, }) // b -> c (causal) InsertRelationSafe(db, &Relation{ ID: "bc", Name: "T2", FromEntity: "b", ToEntity: "c", Via: "fn2", Purity: "impure", Direction: DirUnidirectional, Status: RelDesigned, }) // c -> a (causal) should fail — creates cycle err := InsertRelationSafe(db, &Relation{ ID: "ca", Name: "T3", FromEntity: "c", ToEntity: "a", Via: "fn3", Purity: "impure", Direction: DirUnidirectional, Status: RelDesigned, }) if err == nil { t.Fatal("expected cycle error, got nil") } } func TestCycleDetectionSemanticAllowed(t *testing.T) { db := tempDB(t) db.InsertTypeSnapshot(&TypeSnapshot{ID: "t1", Version: "1.0.0", Lang: "go", Algebraic: "product"}) db.InsertEntity(&Entity{ID: "juan", Name: "juan", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "paula", Name: "paula", TypeRef: "t1", Status: StatusActive, Source: "test"}) // juan -> paula (semantic, no via) if err := InsertRelationSafe(db, &Relation{ ID: "jp", Name: "CONOCE A", FromEntity: "juan", ToEntity: "paula", Direction: DirBidirectional, Status: RelRunning, }); err != nil { t.Fatalf("insert semantic: %v", err) } // paula -> juan (semantic, no via) — should succeed, no cycle check if err := InsertRelationSafe(db, &Relation{ ID: "pj", Name: "CONOCE A", FromEntity: "paula", ToEntity: "juan", Direction: DirBidirectional, Status: RelRunning, }); err != nil { t.Fatalf("semantic cycle should be allowed: %v", err) } } func TestValidateEntity(t *testing.T) { tests := []struct { name string entity Entity wantErr bool }{ { name: "valid", entity: Entity{ID: "x", Name: "x", TypeRef: "t1", Status: StatusActive, Source: "test"}, wantErr: false, }, { name: "missing name", entity: Entity{ID: "x", TypeRef: "t1", Status: StatusActive, Source: "test"}, wantErr: true, }, { name: "missing source", entity: Entity{ID: "x", Name: "x", TypeRef: "t1", Status: StatusActive}, wantErr: true, }, { name: "missing type_ref", entity: Entity{ID: "x", Name: "x", Status: StatusActive, Source: "test"}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateEntity(&tt.entity) if (err != nil) != tt.wantErr { t.Errorf("ValidateEntity() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestGetEntityGraph(t *testing.T) { db := tempDB(t) db.InsertTypeSnapshot(&TypeSnapshot{ID: "t1", Version: "1.0.0", Lang: "go", Algebraic: "product"}) db.InsertEntity(&Entity{ID: "a", Name: "a", TypeRef: "t1", Status: StatusActive, Source: "test"}) db.InsertEntity(&Entity{ID: "b", Name: "b", TypeRef: "t1", Status: StatusActive, Source: "test"}) InsertRelationSafe(db, &Relation{ ID: "ab", Name: "FLUYE", FromEntity: "a", ToEntity: "b", Direction: DirUnidirectional, Status: RelDesigned, }) g, err := GetEntityGraph(db) if err != nil { t.Fatalf("graph: %v", err) } if len(g.Entities) != 2 { t.Errorf("entities = %d, want 2", len(g.Entities)) } if len(g.Relations) != 1 { t.Errorf("relations = %d, want 1", len(g.Relations)) } }