package config import ( "strings" "testing" "time" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) func TestResolveRejectsEmptyAndUnknownPipelineID(t *testing.T) { tests := []struct { name string pipelineID string want string }{ {name: "empty", pipelineID: " ", want: "pipeline id"}, {name: "unknown", pipelineID: "missing", want: "not configured"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := validConfig().Resolve(ResolveInput{PipelineID: tc.pipelineID, Catalog: fakeCatalog(t)}) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("expected error containing %q, got %v", tc.want, err) } }) } } func TestResolveLaneFilteringSuccessAndFailure(t *testing.T) { effective, err := validConfig().Resolve(ResolveInput{ PipelineID: " example ", Only: []string{" notes "}, Catalog: fakeCatalog(t), }) if err != nil { t.Fatalf("Resolve: %v", err) } if effective.PipelineID != "example" { t.Fatalf("unexpected pipeline ID: %q", effective.PipelineID) } if len(effective.ResolvedPipeline.ArtifactLanes) != 1 || effective.ResolvedPipeline.ArtifactLanes[0].ID != "notes" { t.Fatalf("unexpected resolved lanes: %+v", effective.ResolvedPipeline.ArtifactLanes) } if effective.ResolvedPipeline.Digest == "" { t.Fatalf("expected digest") } _, err = validConfig().Resolve(ResolveInput{ PipelineID: "example", Only: []string{"missing"}, Catalog: fakeCatalog(t), }) if err == nil || !strings.Contains(err.Error(), "selected artifact lane") { t.Fatalf("expected invalid lane error, got %v", err) } } func TestResolveUsesTrimmedPipelineMapKeys(t *testing.T) { cfg := validConfig() cfg.Pipelines[" example "] = cfg.Pipelines["example"] delete(cfg.Pipelines, "example") effective, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: fakeCatalog(t)}) if err != nil { t.Fatalf("Resolve: %v", err) } if effective.PipelineID != "example" { t.Fatalf("unexpected pipeline ID: %q", effective.PipelineID) } } func TestResolveSurfacesUnknownModuleKeyThroughCatalog(t *testing.T) { cfg := validConfig() lane := cfg.Pipelines["example"].Artifacts["events"] lane.Extract = pipeline.Binding("missing/extract") cfg.Pipelines["example"].Artifacts["events"] = lane _, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: fakeCatalog(t)}) if err == nil || !strings.Contains(err.Error(), "missing/extract") || !strings.Contains(err.Error(), "events") { t.Fatalf("expected unknown module error with lane context, got %v", err) } } func TestResolveSurfacesMissingCapabilityThroughCatalog(t *testing.T) { _, err := validConfig().Resolve(ResolveInput{ PipelineID: "example", Catalog: fakeCatalog(t, pipeline.ModuleSpec{ Key: "json", Stage: pipeline.StageOutput, Requires: []string{"missing-capability"}, }), }) if err == nil || !strings.Contains(err.Error(), "missing capability") || !strings.Contains(err.Error(), "json") { t.Fatalf("expected missing capability error, got %v", err) } } func TestResolveCanBindSceneChunkerFromCatalog(t *testing.T) { cfg := validConfig() profile := cfg.Pipelines["example"] profile.Chunk = pipeline.Binding("dnd/scenes") lane := profile.Artifacts["events"] profile.Artifacts = map[string]pipeline.ArtifactLaneProfile{"events": lane} cfg.Pipelines["example"] = profile catalog := fakeCatalog(t, pipeline.ModuleSpec{ Key: "fake/input", Stage: pipeline.StageInput, Provides: []string{"source.transcript"}, }, pipeline.ModuleSpec{ Key: "fake/extract", Stage: pipeline.StageExtract, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"artifact"}, }, ) mustRegisterChunker(t, catalog.Chunkers, pipeline.ModuleSpec{ Key: "dnd/scenes", Stage: pipeline.StageChunk, Requires: []string{"source.transcript"}, Provides: []string{"chunks", "chunks.scenes"}, }) effective, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: catalog}) if err != nil { t.Fatalf("Resolve() error = %v, want nil", err) } if got := effective.ResolvedPipeline.Chunk.Module; got != "dnd/scenes" { t.Fatalf("Chunk.Module = %q, want dnd/scenes", got) } } func TestResolveDigestChangesWhenEffectiveConfigChanges(t *testing.T) { cfg := validConfig() first, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: fakeCatalog(t)}) if err != nil { t.Fatalf("Resolve first: %v", err) } lane := cfg.Pipelines["example"].Artifacts["events"] lane.Extract.Options = map[string]any{"temperature": 0.2} cfg.Pipelines["example"].Artifacts["events"] = lane second, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: fakeCatalog(t)}) if err != nil { t.Fatalf("Resolve second: %v", err) } if first.ResolvedPipeline.Digest == second.ResolvedPipeline.Digest { t.Fatalf("expected digest to change, got %q", first.ResolvedPipeline.Digest) } } func TestResolveLLMProfileOverrideAppliesBeforeDigest(t *testing.T) { cfg := validConfig() cfg.LLMProfiles["runtime"] = LLMProfile{Provider: "openai-compatible"} base, err := cfg.Resolve(ResolveInput{PipelineID: "example", Catalog: fakeCatalog(t)}) if err != nil { t.Fatalf("Resolve base: %v", err) } effective, err := cfg.Resolve(ResolveInput{ PipelineID: "example", Catalog: fakeCatalog(t), LLMProfileOverride: "runtime", }) if err != nil { t.Fatalf("Resolve override: %v", err) } if base.ResolvedPipeline.Digest == effective.ResolvedPipeline.Digest { t.Fatalf("expected digest to change after LLM profile override") } for _, binding := range resolvedBindings(effective.ResolvedPipeline) { if binding.LLMProfile != "runtime" { t.Fatalf("binding profile = %q, want runtime", binding.LLMProfile) } } _, err = cfg.Resolve(ResolveInput{ PipelineID: "example", Catalog: fakeCatalog(t), LLMProfileOverride: "missing", }) if err == nil || !strings.Contains(err.Error(), "LLM profile override") { t.Fatalf("expected override profile error, got %v", err) } } func resolvedBindings(resolved pipeline.ResolvedPipeline) []pipeline.ModuleBinding { bindings := []pipeline.ModuleBinding{resolved.Input, resolved.Chunk, resolved.Output} for _, lane := range resolved.ArtifactLanes { bindings = append(bindings, lane.Extract, lane.Merge, lane.Normalize) bindings = append(bindings, lane.Validators...) } return bindings } func TestOpenAICompatibleClientConfigRejectsIncompleteDefaultProfile(t *testing.T) { cfg := Default() _, err := cfg.OpenAICompatibleClientConfig("default") if err == nil || !strings.Contains(err.Error(), "base URL") { t.Fatalf("expected incomplete profile error, got %v", err) } } func TestOpenAICompatibleClientConfigSuccess(t *testing.T) { cfg := validConfig() profile := cfg.LLMProfiles["default"] profile.APIKey = "secret" profile.TimeoutSeconds = 45 profile.MaxRetries = 4 cfg.LLMProfiles["default"] = profile llmCfg, err := cfg.OpenAICompatibleClientConfig(" default ") if err != nil { t.Fatalf("OpenAICompatibleClientConfig: %v", err) } if llmCfg.BaseURL != "https://example.invalid/v1" || llmCfg.Model != "test-model" || llmCfg.APIKey != "secret" { t.Fatalf("unexpected client config strings: %+v", llmCfg) } if llmCfg.MaxRetries != 4 { t.Fatalf("unexpected max retries: %d", llmCfg.MaxRetries) } if llmCfg.RequestTimeout != 45*time.Second { t.Fatalf("unexpected timeout: %s", llmCfg.RequestTimeout) } } func TestOpenAICompatibleClientConfigRejectsUnknownAndUnsupportedProfiles(t *testing.T) { _, err := validConfig().OpenAICompatibleClientConfig("missing") if err == nil || !strings.Contains(err.Error(), "not configured") { t.Fatalf("expected unknown profile error, got %v", err) } cfg := validConfig() profile := cfg.LLMProfiles["default"] profile.Provider = "unsupported" cfg.LLMProfiles["default"] = profile _, err = cfg.OpenAICompatibleClientConfig("default") if err == nil || !strings.Contains(err.Error(), "provider") { t.Fatalf("expected unsupported provider error, got %v", err) } }