diff --git a/registry/validate_test.go b/registry/validate_test.go new file mode 100644 index 00000000..92c0d7d9 --- /dev/null +++ b/registry/validate_test.go @@ -0,0 +1,258 @@ +package registry + +import ( + "strings" + "testing" +) + +func boolPtr(b bool) *bool { return &b } + +func knownFns(ids ...string) map[string]bool { + m := make(map[string]bool) + for _, id := range ids { + m[id] = true + } + return m +} + +func knownTps(ids ...string) map[string]bool { + return knownFns(ids...) +} + +func TestValidateFunction_Valid(t *testing.T) { + f := &Function{ + ID: "filter_slice_go_core", Name: "filter_slice", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "Filtra un slice", Version: "1.0.0", + } + if err := ValidateFunction(f, knownFns(), knownTps()); err != nil { + t.Errorf("expected valid, got: %v", err) + } +} + +func TestValidateFunction_PipelineMustBeImpure(t *testing.T) { + f := &Function{ + ID: "p_go_core", Name: "p", Kind: KindPipeline, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "bad pipeline", Version: "1.0.0", + UsesFunctions: []string{"filter_slice_go_core"}, + } + err := ValidateFunction(f, knownFns("filter_slice_go_core"), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "pipeline must be impure") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_PipelineNeedsUsesFunctions(t *testing.T) { + f := &Function{ + ID: "p_go_core", Name: "p", Kind: KindPipeline, + Lang: "go", Domain: "core", Purity: PurityImpure, + Description: "bad pipeline", Version: "1.0.0", + ErrorType: "error_go_core", + } + err := ValidateFunction(f, knownFns(), knownTps("error_go_core")) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "uses_functions cannot be empty") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_PureNoReturnsOptional(t *testing.T) { + f := &Function{ + ID: "f_go_core", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "bad", Version: "1.0.0", + ReturnsOptional: true, + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "returns_optional") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_PureNoErrorType(t *testing.T) { + f := &Function{ + ID: "f_go_core", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "bad", Version: "1.0.0", + ErrorType: "error_go_core", + } + err := ValidateFunction(f, knownFns(), knownTps("error_go_core")) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "pure function cannot have error_type") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_ImpureNeedsErrorType(t *testing.T) { + f := &Function{ + ID: "f_go_io", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "io", Purity: PurityImpure, + Description: "bad", Version: "1.0.0", + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "impure function must declare error_type") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_TestedNeedsTestFile(t *testing.T) { + f := &Function{ + ID: "f_go_core", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "test", Version: "1.0.0", + Tested: true, + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "test_file_path") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_NotTestedNoTests(t *testing.T) { + f := &Function{ + ID: "f_go_core", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "test", Version: "1.0.0", + Tested: false, Tests: []string{"ghost test"}, TestFilePath: "test.go", + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "tested: false but tests is not empty") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateFunction_OrphanRefs(t *testing.T) { + f := &Function{ + ID: "p_go_core", Name: "p", Kind: KindPipeline, + Lang: "go", Domain: "core", Purity: PurityImpure, + Description: "pipeline", Version: "1.0.0", + UsesFunctions: []string{"nonexistent_go_core"}, + UsesTypes: []string{"ghost_go_core"}, + Returns: []string{"phantom_go_core"}, + ErrorType: "missing_go_core", + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error") + } + if len(err.Errors) < 4 { + t.Errorf("expected at least 4 errors, got %d: %v", len(err.Errors), err) + } +} + +func TestValidateFunction_ComponentRules(t *testing.T) { + f := &Function{ + ID: "dt_typescript_core", Name: "DataTable", Kind: KindComponent, + Lang: "typescript", Domain: "core", Purity: PurityImpure, + Description: "table", Version: "1.0.0", + HasState: boolPtr(true), Framework: "react", + } + if err := ValidateFunction(f, knownFns(), knownTps()); err != nil { + t.Errorf("expected valid, got: %v", err) + } + + // Missing framework + f2 := *f + f2.Framework = "" + if err := ValidateFunction(&f2, knownFns(), knownTps()); err == nil { + t.Error("expected error for missing framework") + } + + // Returns should be empty + f3 := *f + f3.Returns = []string{"some_go_core"} + if err := ValidateFunction(&f3, knownFns(), knownTps("some_go_core")); err == nil { + t.Error("expected error for non-empty returns on component") + } + + // has_state: true but pure + f4 := *f + f4.Purity = PurityPure + if err := ValidateFunction(&f4, knownFns(), knownTps()); err == nil { + t.Error("expected error for stateful pure component") + } +} + +func TestValidateFunction_AbsoluteFilePath(t *testing.T) { + f := &Function{ + ID: "f_go_core", Name: "f", Kind: KindFunction, + Lang: "go", Domain: "core", Purity: PurityPure, + Description: "test", Version: "1.0.0", + FilePath: "/absolute/path.go", + } + err := ValidateFunction(f, knownFns(), knownTps()) + if err == nil { + t.Fatal("expected error for absolute file_path") + } +} + +func TestValidateType_Valid(t *testing.T) { + typ := &Type{ + ID: "ohlcv_go_finance", Name: "ohlcv", Lang: "go", Domain: "finance", + Algebraic: AlgebraicProduct, Description: "candle", Version: "1.0.0", + } + if err := ValidateType(typ, knownTps("ohlcv_go_finance")); err != nil { + t.Errorf("expected valid, got: %v", err) + } +} + +func TestValidateType_BadAlgebraic(t *testing.T) { + typ := &Type{ + ID: "t_go_core", Name: "t", Lang: "go", Domain: "core", + Algebraic: "wrong", Description: "bad", Version: "1.0.0", + } + err := ValidateType(typ, knownTps("t_go_core")) + if err == nil { + t.Fatal("expected error") + } +} + +func TestValidateType_SelfReference(t *testing.T) { + typ := &Type{ + ID: "t_go_core", Name: "t", Lang: "go", Domain: "core", + Algebraic: AlgebraicProduct, Description: "self ref", Version: "1.0.0", + UsesTypes: []string{"t_go_core"}, + } + err := ValidateType(typ, knownTps("t_go_core")) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "cannot reference itself") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidateType_OrphanRef(t *testing.T) { + typ := &Type{ + ID: "t_go_core", Name: "t", Lang: "go", Domain: "core", + Algebraic: AlgebraicProduct, Description: "orphan ref", Version: "1.0.0", + UsesTypes: []string{"nonexistent_go_core"}, + } + err := ValidateType(typ, knownTps("t_go_core")) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "unknown type") { + t.Errorf("unexpected error: %v", err) + } +}