package config import ( "strings" "testing" "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() profile := cfg.Pipelines["example"] profile.Input.LLMProfile = "input-profile" profile.Output.LLMProfile = "output-profile" lane := profile.Artifacts["events"] lane.Merge.LLMProfile = "merge-profile" lane.Validators[0].LLMProfile = "validator-profile" profile.Artifacts["events"] = lane cfg.Pipelines["example"] = profile 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 llmCapableBindings(effective.ResolvedPipeline) { if binding.LLMProfile != "runtime" { t.Fatalf("LLM-capable binding profile = %q, want runtime", binding.LLMProfile) } } if effective.ResolvedPipeline.Input.LLMProfile != "input-profile" { t.Fatalf("input profile = %q, want original input-profile", effective.ResolvedPipeline.Input.LLMProfile) } if effective.ResolvedPipeline.Output.LLMProfile != "output-profile" { t.Fatalf("output profile = %q, want original output-profile", effective.ResolvedPipeline.Output.LLMProfile) } eventLane := effective.ResolvedPipeline.ArtifactLanes[0] if eventLane.Merge.LLMProfile != "merge-profile" { t.Fatalf("merge profile = %q, want original merge-profile", eventLane.Merge.LLMProfile) } if len(eventLane.Validators) != 1 || eventLane.Validators[0].LLMProfile != "validator-profile" { t.Fatalf("validator profiles = %#v, want original validator-profile", eventLane.Validators) } } func llmCapableBindings(resolved pipeline.ResolvedPipeline) []pipeline.ModuleBinding { bindings := []pipeline.ModuleBinding{resolved.Chunk} for _, lane := range resolved.ArtifactLanes { bindings = append(bindings, lane.Extract, lane.Normalize) } return bindings }