Wire production CLI catalog and scheduled LLM client
This commit is contained in:
@@ -2,13 +2,22 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/chunk/generic"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/input/seriatim"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/merge/appendorder"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/normalize/noop"
|
||||
jsonoutput "gitea.maximumdirect.net/eric/notarius/internal/modules/output/json"
|
||||
)
|
||||
|
||||
func TestRunNoArgsWritesUsageToStdout(t *testing.T) {
|
||||
@@ -102,6 +111,95 @@ func TestRunConfigValidateSuccessWithFakeCatalog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionCatalogIncludesDefaultModules(t *testing.T) {
|
||||
catalog, err := productionCatalog()
|
||||
if err != nil {
|
||||
t.Fatalf("productionCatalog() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
got func() (pipeline.ModuleSpec, bool)
|
||||
want pipeline.ModuleSpec
|
||||
}{
|
||||
{
|
||||
name: "seriatim input",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Inputs.Spec(seriatim.Key) },
|
||||
want: seriatim.ModuleSpec(),
|
||||
},
|
||||
{
|
||||
name: "generic chunker",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Chunkers.Spec(generic.Key) },
|
||||
want: generic.ModuleSpec(),
|
||||
},
|
||||
{
|
||||
name: "dnd spells extractor",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Extractors.Spec(spells.Key) },
|
||||
want: spells.ModuleSpec(),
|
||||
},
|
||||
{
|
||||
name: "appendorder merger",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Mergers.Spec(appendorder.Key) },
|
||||
want: appendorder.ModuleSpec(),
|
||||
},
|
||||
{
|
||||
name: "noop normalizer",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Normalizers.Spec(noop.Key) },
|
||||
want: noop.ModuleSpec(),
|
||||
},
|
||||
{
|
||||
name: "json output",
|
||||
got: func() (pipeline.ModuleSpec, bool) { return catalog.Outputs.Spec(jsonoutput.Key) },
|
||||
want: jsonoutput.ModuleSpec(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, ok := test.got()
|
||||
if !ok {
|
||||
t.Fatalf("module spec ok = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("module spec = %#v, want %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateUsesProductionCatalogByDefault(t *testing.T) {
|
||||
configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells"))
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "dnd-session"}, &stdout, &stderr, Options{})
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "is valid for pipeline") {
|
||||
t.Fatalf("stdout = %q, want validation success", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateUnknownProductionModuleIncludesContext(t *testing.T) {
|
||||
configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "missing/extract"))
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "dnd-session"}, &stdout, &stderr, Options{})
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("RunWithOptions() code = %d, want 1", code)
|
||||
}
|
||||
got := stderr.String()
|
||||
for _, want := range []string{"dnd-session", "extract", "missing/extract", "not registered"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("stderr = %q, want substring %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateReportsParseErrors(t *testing.T) {
|
||||
configPath := writeFile(t, "config.yml", "version: 2\n")
|
||||
var stdout bytes.Buffer
|
||||
@@ -320,6 +418,83 @@ func TestRunInvalidFlagsExitTwo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionLLMClientFactoryRejectsMissingProfile(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
|
||||
_, _, err := productionLLMClientFactory(context.Background(), cfg, "missing")
|
||||
if err == nil {
|
||||
t.Fatal("productionLLMClientFactory() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "LLM profile") || !strings.Contains(err.Error(), "missing") {
|
||||
t.Fatalf("error = %q, want missing profile context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionLLMClientFactoryRejectsInvalidProfile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
profile config.LLMProfile
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "unsupported provider",
|
||||
profile: config.LLMProfile{Provider: "other", BaseURL: "https://example.test", Model: "model"},
|
||||
want: "not supported",
|
||||
},
|
||||
{
|
||||
name: "missing base url",
|
||||
profile: config.LLMProfile{Provider: "openai-compatible", Model: "model"},
|
||||
want: "base URL",
|
||||
},
|
||||
{
|
||||
name: "missing model",
|
||||
profile: config.LLMProfile{Provider: "openai-compatible", BaseURL: "https://example.test"},
|
||||
want: "model",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
cfg.LLMProfiles = map[string]config.LLMProfile{"default": test.profile}
|
||||
|
||||
_, _, err := productionLLMClientFactory(context.Background(), cfg, "default")
|
||||
if err == nil {
|
||||
t.Fatal("productionLLMClientFactory() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("error = %q, want substring %q", err.Error(), test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionLLMClientFactoryReturnsScheduledClientAndManifestMetadata(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
cfg.LLMProfiles = map[string]config.LLMProfile{
|
||||
"default": {
|
||||
Provider: "openai-compatible",
|
||||
BaseURL: "https://example.test",
|
||||
Model: "model-a",
|
||||
MaxConcurrency: 2,
|
||||
},
|
||||
}
|
||||
|
||||
client, metadata, err := productionLLMClientFactory(context.Background(), cfg, "default")
|
||||
if err != nil {
|
||||
t.Fatalf("productionLLMClientFactory() error = %v, want nil", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("client = nil, want scheduled client")
|
||||
}
|
||||
if len(metadata) != 1 {
|
||||
t.Fatalf("len(metadata) = %d, want 1", len(metadata))
|
||||
}
|
||||
if metadata[0].ID != "default" || metadata[0].Provider != "openai-compatible" || metadata[0].Model != "model-a" {
|
||||
t.Fatalf("metadata = %#v, want profile-safe model metadata", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestConfig(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
return writeFile(t, "config.yml", content)
|
||||
@@ -354,6 +529,17 @@ func testConfigYAMLForPipelines(pipelines map[string][]string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func mvpConfigYAML(pipelineID string, extractor string) string {
|
||||
return `version: 1
|
||||
pipelines:
|
||||
` + pipelineID + `:
|
||||
input: seriatim
|
||||
artifacts:
|
||||
spells:
|
||||
extract: ` + extractor + `
|
||||
`
|
||||
}
|
||||
|
||||
func fakeCatalog(t *testing.T) pipeline.ModuleCatalog {
|
||||
t.Helper()
|
||||
inputs := pipeline.NewInputAdapterRegistry()
|
||||
|
||||
Reference in New Issue
Block a user