252 lines
7.9 KiB
Go
252 lines
7.9 KiB
Go
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)
|
|
}
|
|
}
|