package cli import ( "bytes" "context" "encoding/json" "errors" "os" "path/filepath" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/core/config" "gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics" "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) { var stdout bytes.Buffer var stderr bytes.Buffer code := Run(nil, &stdout, &stderr) if code != 0 { t.Fatalf("Run() code = %d, want 0", code) } if stdout.String() != usage { t.Fatalf("stdout = %q, want %q", stdout.String(), usage) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestRunHelpArgsWriteUsageToStdout(t *testing.T) { tests := []struct { name string args []string }{ {name: "help", args: []string{"help"}}, {name: "long help flag", args: []string{"--help"}}, {name: "short help flag", args: []string{"-h"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Run(tt.args, &stdout, &stderr) if code != 0 { t.Fatalf("Run() code = %d, want 0", code) } if stdout.String() != usage { t.Fatalf("stdout = %q, want %q", stdout.String(), usage) } if !strings.Contains(stdout.String(), "config validate") || !strings.Contains(stdout.String(), "pipelines list") { t.Fatalf("usage does not mention new commands: %q", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } }) } } func TestRunUnknownCommandWritesErrorAndUsageToStderr(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Run([]string{"extract"}, &stdout, &stderr) if code != 2 { t.Fatalf("Run() code = %d, want 2", code) } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } gotStderr := stderr.String() if !strings.Contains(gotStderr, "notarius: unknown command \"extract\"") { t.Fatalf("stderr = %q, want unknown command error", gotStderr) } if !strings.Contains(gotStderr, usage) { t.Fatalf("stderr = %q, want usage", gotStderr) } } func TestRunConfigValidateSuccessWithFakeCatalog(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events", "notes")) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "example"}, &stdout, &stderr, Options{ Catalog: fakeCatalog(t), }) 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()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } 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 var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath}, &stdout, &stderr, Options{}) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } if !strings.Contains(stderr.String(), "unsupported config version") { t.Fatalf("stderr = %q, want parse error", stderr.String()) } } func TestRunConfigValidatePipelineOnlySuccessAndInvalidLane(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events", "notes")) t.Run("success", func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "example", "--only", "notes"}, &stdout, &stderr, Options{ Catalog: fakeCatalog(t), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } }) t.Run("invalid lane", func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "example", "--only", "missing"}, &stdout, &stderr, Options{ Catalog: fakeCatalog(t), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "selected artifact lane") { t.Fatalf("stderr = %q, want invalid lane error", stderr.String()) } }) } func TestRunConfigValidateOnlyWithoutPipelineFails(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events")) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--only", "events"}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "--only requires --pipeline") { t.Fatalf("stderr = %q, want only/pipeline error", stderr.String()) } } func TestRunConfigValidateRejectsMalformedOnlyValues(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events", "notes")) tests := []string{",", "notes,", ",notes", "events, ,notes"} for _, only := range tests { t.Run(only, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "example", "--only", only}, &stdout, &stderr, Options{ Catalog: fakeCatalog(t), }) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } if !strings.Contains(stderr.String(), "--only") { t.Fatalf("stderr = %q, want --only error", stderr.String()) } }) } } func TestRunPipelinesListSortedTextOutput(t *testing.T) { configPath := writeTestConfig(t, testConfigYAMLForPipelines(map[string][]string{ "zeta": {"events"}, "alpha": {"events"}, })) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"pipelines", "list", "--config", configPath}, &stdout, &stderr, Options{}) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if got, want := stdout.String(), "alpha\nzeta\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } } func TestRunPipelinesListStableJSONOutput(t *testing.T) { configPath := writeTestConfig(t, testConfigYAMLForPipelines(map[string][]string{ "b": {"events"}, "a": {"events"}, })) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"pipelines", "list", "--config", configPath, "--json"}, &stdout, &stderr, Options{}) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if got, want := stdout.String(), "{\"pipelines\":[\"a\",\"b\"]}\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } } func TestRunUsesNotariusConfigWhenConfigFlagAbsent(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events")) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"pipelines", "list"}, &stdout, &stderr, Options{ LookupEnv: mapLookup(map[string]string{"NOTARIUS_CONFIG": configPath}), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if got, want := stdout.String(), "example\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } } func TestRunConfigValidateResolvesAPIKeyEnvThroughOptions(t *testing.T) { configPath := writeTestConfig(t, `version: 1 llm_profiles: default: api_key_env: NOTARIUS_TEST_API_KEY pipelines: example: input: fake/input artifacts: events: extract: fake/extract `) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath}, &stdout, &stderr, Options{ LookupEnv: mapLookup(map[string]string{"NOTARIUS_TEST_API_KEY": "secret"}), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } } func TestRunMissingConfigPathProducesActionableError(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", filepath.Join(t.TempDir(), "missing.yml")}, &stdout, &stderr, Options{}) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "config file") || !strings.Contains(stderr.String(), "not available") { t.Fatalf("stderr = %q, want actionable missing config error", stderr.String()) } } func TestRunRejectsAdHocStructuralFlags(t *testing.T) { configPath := writeTestConfig(t, testConfigYAML("example", "events")) flags := []string{"--extractor", "--chunker", "--input", "--merge", "--normalize"} for _, flagName := range flags { t.Run(flagName, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"config", "validate", "--config", configPath, flagName, "value"}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "flag provided but not defined") { t.Fatalf("stderr = %q, want invalid flag error", stderr.String()) } }) } } func TestRunInvalidFlagsExitTwo(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"pipelines", "list", "--bogus"}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "flag provided but not defined") { t.Fatalf("stderr = %q, want invalid flag error", stderr.String()) } } 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 TestRunPipelineMissingPipelineID(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "--config", configPath, "--input", inputPath}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "pipeline ID") { t.Fatalf("stderr = %q, want missing pipeline ID error", stderr.String()) } } func TestRunPipelineMissingInputFlag(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "--input") { t.Fatalf("stderr = %q, want missing input error", stderr.String()) } } func TestRunPipelineRejectsUnknownFlag(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--extractor", "dnd/spells"}, &stdout, &stderr, Options{}) if code != 2 { t.Fatalf("RunWithOptions() code = %d, want 2", code) } if !strings.Contains(stderr.String(), "flag provided but not defined") { t.Fatalf("stderr = %q, want invalid flag error", stderr.String()) } } func TestRunPipelineUnknownPipeline(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) diagnosticsDir := t.TempDir() var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "missing", "--config", configPath, "--input", inputPath, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "pipeline \"missing\" is not configured") { t.Fatalf("stderr = %q, want unknown pipeline error", stderr.String()) } } func TestRunPipelineUnknownOnlyLane(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) diagnosticsDir := t.TempDir() var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--only", "missing", "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "selected artifact lane") { t.Fatalf("stderr = %q, want selected lane error", stderr.String()) } } func TestRunPipelineInvalidInputPath(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := filepath.Join(t.TempDir(), "missing.json") diagnosticsDir := t.TempDir() var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "read input") { t.Fatalf("stderr = %q, want input read error", stderr.String()) } } func TestRunPipelineSuccessUsesProductionRegistriesAndFakeLLM(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) outputDir := t.TempDir() diagnosticsDir := t.TempDir() client := newFakeRunLLMClient(false) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(client, nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if client.calls != 1 { t.Fatalf("LLM calls = %d, want 1", client.calls) } for _, want := range []string{"dnd-session", "approved=1", "rejected=0", outputDir} { if !strings.Contains(stdout.String(), want) { t.Fatalf("stdout = %q, want substring %q", stdout.String(), want) } } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestRunPipelineOnlySelectsRequestedLane(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAMLForLanes("dnd-session", "spells", "rituals")) inputPath := writeSeriatimInput(t) outputDir := t.TempDir() diagnosticsDir := t.TempDir() client := newFakeRunLLMClient(false) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--only", "spells", "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(client, nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if client.calls != 1 { t.Fatalf("LLM calls = %d, want only selected lane to run once", client.calls) } if !strings.Contains(stdout.String(), "approved=1") { t.Fatalf("stdout = %q, want approved count", stdout.String()) } } func TestRunPipelineLLMFactoryFailure(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) diagnosticsDir := t.TempDir() var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), errors.New("factory unavailable")), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "create LLM client") || !strings.Contains(stderr.String(), "factory unavailable") { t.Fatalf("stderr = %q, want LLM factory error", stderr.String()) } } func TestRunPipelineValidationRejectionCompletesSuccessfully(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAML("dnd-session", "dnd/spells")) inputPath := writeSeriatimInput(t) outputDir := t.TempDir() diagnosticsDir := t.TempDir() client := newFakeRunLLMClient(true) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(client, nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "approved=0") || !strings.Contains(stdout.String(), "rejected=1") { t.Fatalf("stdout = %q, want rejection counts", stdout.String()) } } func TestRunPipelineLLMProfileOverrideSelectsFactoryProfile(t *testing.T) { configPath := writeTestConfig(t, mvpConfigYAMLWithProfiles("dnd-session")) inputPath := writeSeriatimInput(t) outputDir := t.TempDir() diagnosticsDir := t.TempDir() client := newFakeRunLLMClient(false) factory := &recordingLLMFactory{client: client} var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--llm-profile", "runtime", "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: factory.build, }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if got, want := strings.Join(factory.profileIDs, ","), "runtime"; got != want { t.Fatalf("factory profile IDs = %q, want %q", got, want) } } func TestRunPipelineWritesDurableOutputFiles(t *testing.T) { diagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } runOutputDir := onlyChildDir(t, outputDir) for _, name := range []string{ "index.json", "manifest.json", "artifacts/dnd.spell_cast.json", "rejected.json", "warnings.json", } { if _, err := os.Stat(filepath.Join(runOutputDir, filepath.FromSlash(name))); err != nil { t.Fatalf("expected output file %q: %v", name, err) } } if !strings.Contains(stdout.String(), runOutputDir) { t.Fatalf("stdout = %q, want output path %q", stdout.String(), runOutputDir) } assertNoTemporaryFiles(t, runOutputDir) } func TestRunPipelineRejectsUnsafeOutputFileName(t *testing.T) { diagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always")) inputPath := writeSeriatimInput(t) registries := registriesWithOutput(t, unsafeOutputEncoder{}) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{ Registries: registries, LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "output file name") { t.Fatalf("stderr = %q, want unsafe output file error", stderr.String()) } runDir := onlyChildDir(t, diagnosticsDir) if got := string(readFile(t, filepath.Join(runDir, diagnostics.ArtifactErrorLog))); !strings.Contains(got, "output file name") { t.Fatalf("error log = %q, want unsafe output file error", got) } if _, err := os.Stat(filepath.Join(runDir, diagnostics.ArtifactRunManifest)); err != nil { t.Fatalf("expected diagnostics manifest after unsafe output file failure: %v", err) } } func TestRunPipelineWritesDiagnosticsArtifactsWhenDurableOutputWriteFails(t *testing.T) { diagnosticsDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always")) inputPath := writeSeriatimInput(t) outputRootFile := writeFile(t, "not-a-directory", "occupied") var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputRootFile}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } if !strings.Contains(stderr.String(), "create output directory") { t.Fatalf("stderr = %q, want output directory error", stderr.String()) } runDir := onlyChildDir(t, diagnosticsDir) for _, name := range []string{ diagnostics.ArtifactRunManifest, diagnostics.ArtifactWarnings, diagnostics.ArtifactRunReport, diagnostics.ArtifactErrorLog, } { if _, err := os.Stat(filepath.Join(runDir, name)); err != nil { t.Fatalf("expected diagnostics artifact %q after durable output write failure: %v", name, err) } } } func TestRunPipelineWritesDiagnosticsArtifactsOnSuccess(t *testing.T) { diagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } runDir := onlyChildDir(t, diagnosticsDir) for _, name := range []string{ diagnostics.ArtifactInvocationMetadata, diagnostics.ArtifactEffectiveConfig, diagnostics.ArtifactResolvedPipeline, diagnostics.ArtifactRunManifest, diagnostics.ArtifactRunReport, diagnostics.ArtifactWarnings, } { if _, err := os.Stat(filepath.Join(runDir, name)); err != nil { t.Fatalf("expected diagnostics artifact %q: %v", name, err) } } report := string(readFile(t, filepath.Join(runDir, diagnostics.ArtifactRunReport))) if !strings.Contains(report, `"approved_count": 1`) || !strings.Contains(report, `"validation_status": "approved"`) || !strings.Contains(report, outputDir) { t.Fatalf("unexpected run report: %s", report) } } func TestRunPipelineWritesErrorLogAfterDiagnosticsCreation(t *testing.T) { diagnosticsDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), errors.New("factory unavailable")), }) if code != 1 { t.Fatalf("RunWithOptions() code = %d, want 1", code) } runDir := onlyChildDir(t, diagnosticsDir) if got := string(readFile(t, filepath.Join(runDir, diagnostics.ArtifactErrorLog))); !strings.Contains(got, "factory unavailable") { t.Fatalf("error log = %q, want factory error", got) } } func TestRunPipelineRetentionNeverRemovesSuccessfulWarningFreeDiagnostics(t *testing.T) { diagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "never")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if entries := childDirs(t, diagnosticsDir); len(entries) != 0 { t.Fatalf("diagnostics run dirs = %v, want none", entries) } } func TestRunPipelineWarningsAreDiagnosedAndReported(t *testing.T) { diagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "auto")) inputPath := writeSeriatimInput(t) registries := registriesWithOutput(t, warningOutputEncoder{}) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{ Registries: registries, LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if !strings.Contains(stderr.String(), "1 warning") { t.Fatalf("stderr = %q, want warning count", stderr.String()) } runDir := onlyChildDir(t, diagnosticsDir) warnings := string(readFile(t, filepath.Join(runDir, diagnostics.ArtifactWarnings))) if !strings.Contains(warnings, "synthetic_warning") { t.Fatalf("warnings artifact = %q, want synthetic warning", warnings) } } func TestRunPipelineDiagnosticsDirFlagOverridesConfig(t *testing.T) { configDiagnosticsDir := t.TempDir() overrideDiagnosticsDir := t.TempDir() outputDir := t.TempDir() configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", configDiagnosticsDir, "always")) inputPath := writeSeriatimInput(t) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir, "--diagnostics-dir", overrideDiagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if entries := childDirs(t, configDiagnosticsDir); len(entries) != 0 { t.Fatalf("config diagnostics dir entries = %v, want none", entries) } if entries := childDirs(t, overrideDiagnosticsDir); len(entries) != 1 { t.Fatalf("override diagnostics dir entries = %v, want one run dir", entries) } } func TestExampleFixtureConfigValidateAndPipelinesList(t *testing.T) { configPath := fixturePath(t, "examples/dnd-spells.config.yml") t.Run("validate configured pipeline", func(t *testing.T) { 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(), "dnd-session") { t.Fatalf("stdout = %q, want pipeline ID", stdout.String()) } }) t.Run("list configured pipelines", func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"pipelines", "list", "--config", configPath}, &stdout, &stderr, Options{}) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if got, want := stdout.String(), "dnd-session\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } }) } func TestExampleFixtureRunWritesExpectedJSON(t *testing.T) { configPath := fixturePath(t, "examples/dnd-spells.config.yml") inputPath := fixturePath(t, "examples/seriatim-minimal-transcript.json") outputDir := t.TempDir() diagnosticsDir := t.TempDir() var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } runOutputDir := onlyChildDir(t, outputDir) if !strings.Contains(stdout.String(), runOutputDir) { t.Fatalf("stdout = %q, want output path %q", stdout.String(), runOutputDir) } var manifest artifacts.RunManifest readJSONFile(t, filepath.Join(runOutputDir, "manifest.json"), &manifest) if manifest.PipelineID != "dnd-session" { t.Fatalf("manifest pipeline ID = %q, want dnd-session", manifest.PipelineID) } if manifest.PipelineDigest == "" { t.Fatal("manifest pipeline digest is empty") } if manifest.ValidationStatus != "approved" { t.Fatalf("validation status = %q, want approved", manifest.ValidationStatus) } if len(manifest.ArtifactLanes) != 1 { t.Fatalf("artifact lanes = %#v, want one lane", manifest.ArtifactLanes) } extractorMetadata, ok := manifest.ArtifactLanes[0].Metadata["extractor"].(map[string]any) if !ok { t.Fatalf("extractor metadata = %#v, want object", manifest.ArtifactLanes[0].Metadata) } if extractorMetadata["prompt_id"] != "dnd.spells" || extractorMetadata["response_schema_key"] != "dnd_spells" { t.Fatalf("extractor metadata = %#v, want prompt/schema identifiers", extractorMetadata) } var artifactFile struct { ArtifactType string `json:"artifact_type"` Artifacts []artifacts.Artifact `json:"artifacts"` } readJSONFile(t, filepath.Join(runOutputDir, "artifacts", "dnd.spell_cast.json"), &artifactFile) if artifactFile.ArtifactType != "dnd.spell_cast" || len(artifactFile.Artifacts) != 1 { t.Fatalf("artifact file = %#v, want one spell artifact", artifactFile) } var payload struct { Caster string `json:"caster"` Spell string `json:"spell"` Effect string `json:"effect"` } if err := json.Unmarshal(artifactFile.Artifacts[0].Payload, &payload); err != nil { t.Fatalf("unmarshal spell payload: %v", err) } if payload.Caster != "Aria" || payload.Spell != "Cure Wounds" || payload.Effect == "" { t.Fatalf("payload = %#v, want deterministic spell output", payload) } if len(artifactFile.Artifacts[0].SourceRefs) != 1 { t.Fatalf("source refs = %#v, want one source ref", artifactFile.Artifacts[0].SourceRefs) } ref := artifactFile.Artifacts[0].SourceRefs[0] if ref.SourceID != "session-alpha" || ref.StartUnitID != "seg-001" || ref.EndUnitID != "seg-001" { t.Fatalf("source ref = %#v, want fixture source ref", ref) } warnings := string(readFile(t, filepath.Join(runOutputDir, "warnings.json"))) if !strings.Contains(warnings, `"warnings": []`) { t.Fatalf("warnings output = %s, want empty warnings", warnings) } } func TestExampleFixtureRunOnlySpells(t *testing.T) { configPath := fixturePath(t, "examples/dnd-spells.config.yml") inputPath := fixturePath(t, "examples/seriatim-minimal-transcript.json") outputDir := t.TempDir() diagnosticsDir := t.TempDir() client := newFakeRunLLMClient(false) var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--only", "spells", "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir}, &stdout, &stderr, Options{ LLMClientFactory: fakeLLMFactory(client, nil), }) if code != 0 { t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String()) } if client.calls != 1 { t.Fatalf("LLM calls = %d, want selected lane once", client.calls) } if _, err := os.Stat(filepath.Join(onlyChildDir(t, outputDir), "manifest.json")); err != nil { t.Fatalf("expected manifest output: %v", err) } } func TestExampleFixtureFailureCoverage(t *testing.T) { configPath := fixturePath(t, "examples/dnd-spells.config.yml") inputPath := fixturePath(t, "examples/seriatim-minimal-transcript.json") tests := []struct { name string args []string factory LLMClientFactory wantCode int wantStderr string wantOutputStatus string }{ { name: "missing config", args: []string{"config", "validate", "--config", filepath.Join(filepath.Dir(configPath), "missing.yml"), "--pipeline", "dnd-session"}, wantCode: 1, wantStderr: "config file", }, { name: "unknown pipeline", args: []string{"run", "missing", "--config", configPath, "--input", inputPath}, factory: fakeLLMFactory(newFakeRunLLMClient(false), nil), wantCode: 1, wantStderr: "not configured", }, { name: "invalid Seriatim input", args: []string{"run", "dnd-session", "--config", configPath, "--input", fixturePath(t, "internal/cli/testdata/invalid-seriatim-empty-segments.json")}, factory: fakeLLMFactory(newFakeRunLLMClient(false), nil), wantCode: 1, wantStderr: "segments must not be empty", }, { name: "invalid only lane", args: []string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--only", "missing"}, factory: fakeLLMFactory(newFakeRunLLMClient(false), nil), wantCode: 1, wantStderr: "selected artifact lane", }, { name: "fake LLM failure", args: []string{"run", "dnd-session", "--config", configPath, "--input", inputPath}, factory: fakeLLMFactory(newErrorRunLLMClient(errors.New("completion unavailable")), nil), wantCode: 1, wantStderr: "completion unavailable", }, { name: "malformed LLM response", args: []string{"run", "dnd-session", "--config", configPath, "--input", inputPath}, factory: fakeLLMFactory(newMalformedRunLLMClient(), nil), wantCode: 1, wantStderr: "spell_casts", }, { name: "invalid source reference rejection", args: []string{"run", "dnd-session", "--config", configPath, "--input", inputPath}, factory: fakeLLMFactory(newFakeRunLLMClient(true), nil), wantCode: 0, wantOutputStatus: "rejected", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { outputDir := t.TempDir() diagnosticsDir := t.TempDir() args := append([]string(nil), test.args...) if args[0] == "run" { args = append(args, "--output-dir", outputDir, "--diagnostics-dir", diagnosticsDir) } factory := test.factory if factory == nil { factory = fakeLLMFactory(newFakeRunLLMClient(false), nil) } var stdout bytes.Buffer var stderr bytes.Buffer code := RunWithOptions(args, &stdout, &stderr, Options{LLMClientFactory: factory}) if code != test.wantCode { t.Fatalf("RunWithOptions() code = %d, want %d; stderr=%q", code, test.wantCode, stderr.String()) } if test.wantStderr != "" && !strings.Contains(stderr.String(), test.wantStderr) { t.Fatalf("stderr = %q, want substring %q", stderr.String(), test.wantStderr) } if test.wantOutputStatus != "" { runOutputDir := onlyChildDir(t, outputDir) var manifest artifacts.RunManifest readJSONFile(t, filepath.Join(runOutputDir, "manifest.json"), &manifest) if manifest.ValidationStatus != test.wantOutputStatus { t.Fatalf("validation status = %q, want %q", manifest.ValidationStatus, test.wantOutputStatus) } rejected := string(readFile(t, filepath.Join(runOutputDir, "rejected.json"))) if !strings.Contains(rejected, "invalid_source_ref") { t.Fatalf("rejected output = %s, want invalid source ref rejection", rejected) } } }) } } func writeTestConfig(t *testing.T, content string) string { t.Helper() return writeFile(t, "config.yml", content) } func writeFile(t *testing.T, name string, content string) string { t.Helper() path := filepath.Join(t.TempDir(), name) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write %s: %v", name, err) } return path } func fixturePath(t *testing.T, name string) string { t.Helper() path := filepath.Join("..", "..", filepath.FromSlash(name)) if _, err := os.Stat(path); err != nil { t.Fatalf("fixture %q is not available at %q: %v", name, path, err) } return path } func testConfigYAML(pipelineID string, laneIDs ...string) string { return testConfigYAMLForPipelines(map[string][]string{pipelineID: laneIDs}) } func testConfigYAMLForPipelines(pipelines map[string][]string) string { var b strings.Builder b.WriteString("version: 1\n") b.WriteString("pipelines:\n") for pipelineID, laneIDs := range pipelines { b.WriteString(" " + pipelineID + ":\n") b.WriteString(" input: fake/input\n") b.WriteString(" artifacts:\n") for _, laneID := range laneIDs { b.WriteString(" " + laneID + ":\n") b.WriteString(" extract: fake/extract\n") } } return b.String() } func mvpConfigYAML(pipelineID string, extractor string) string { return `version: 1 pipelines: ` + pipelineID + `: input: seriatim artifacts: spells: extract: ` + extractor + ` ` } func mvpConfigYAMLForLanes(pipelineID string, laneIDs ...string) string { var b strings.Builder b.WriteString("version: 1\n") b.WriteString("pipelines:\n") b.WriteString(" " + pipelineID + ":\n") b.WriteString(" input: seriatim\n") b.WriteString(" artifacts:\n") for _, laneID := range laneIDs { b.WriteString(" " + laneID + ":\n") b.WriteString(" extract: dnd/spells\n") } return b.String() } func mvpConfigYAMLWithProfiles(pipelineID string) string { return `version: 1 llm_profiles: default: provider: openai-compatible runtime: provider: openai-compatible pipelines: ` + pipelineID + `: input: seriatim artifacts: spells: extract: dnd/spells ` } func mvpConfigYAMLWithDiagnostics(pipelineID, diagnosticsDir, retention string) string { return `version: 1 diagnostics: work_dir: ` + diagnosticsDir + ` retention: ` + retention + ` pipelines: ` + pipelineID + `: input: seriatim artifacts: spells: extract: dnd/spells ` } func writeSeriatimInput(t *testing.T) string { t.Helper() return writeFile(t, "source.json", `{ "metadata": { "id": "session-alpha" }, "segments": [ { "id": "seg-001", "start": 0, "end": 1, "speaker": "Aria", "text": "Aria casts Cure Wounds." } ] }`) } type fakeRunLLMClient struct { invalidSourceRef bool calls int err error payload map[string]any } func newFakeRunLLMClient(invalidSourceRef bool) *fakeRunLLMClient { return &fakeRunLLMClient{invalidSourceRef: invalidSourceRef} } func newErrorRunLLMClient(err error) *fakeRunLLMClient { return &fakeRunLLMClient{err: err} } func newMalformedRunLLMClient() *fakeRunLLMClient { return &fakeRunLLMClient{payload: map[string]any{}} } func (client *fakeRunLLMClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) { client.calls++ if client.err != nil { return contracts.StructuredCompletionResponse{}, client.err } startUnitID := "seg-001" if client.invalidSourceRef { startUnitID = "missing-segment" } payload := client.payload if payload == nil { payload = map[string]any{ "spell_casts": []map[string]any{ { "caster": "Aria", "spell": "Cure Wounds", "effect": "Heals a wounded ally.", "narrative_description": "Aria casts Cure Wounds.", "source_refs": []map[string]string{ { "source_id": "session-alpha", "start_unit_id": startUnitID, "end_unit_id": "seg-001", }, }, }, }, } } encoded, err := json.Marshal(payload) if err != nil { return contracts.StructuredCompletionResponse{}, err } if err := json.Unmarshal(encoded, out); err != nil { return contracts.StructuredCompletionResponse{}, err } return contracts.StructuredCompletionResponse{Content: encoded}, nil } func fakeLLMFactory(client contracts.StructuredLLMClient, err error) LLMClientFactory { return func(ctx context.Context, cfg config.Config, profileID string) (contracts.StructuredLLMClient, []artifacts.LLMProfileManifest, error) { if err != nil { return nil, nil, err } return client, []artifacts.LLMProfileManifest{{ID: strings.TrimSpace(profileID)}}, nil } } type recordingLLMFactory struct { client contracts.StructuredLLMClient profileIDs []string } func (factory *recordingLLMFactory) build(ctx context.Context, cfg config.Config, profileID string) (contracts.StructuredLLMClient, []artifacts.LLMProfileManifest, error) { factory.profileIDs = append(factory.profileIDs, strings.TrimSpace(profileID)) return factory.client, []artifacts.LLMProfileManifest{{ID: strings.TrimSpace(profileID)}}, nil } type unsafeOutputEncoder struct{} func (unsafeOutputEncoder) Key() string { return "json" } func (unsafeOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) { return contracts.OutputResult{ Files: []contracts.OutputFile{ { Name: "../escape.json", ContentType: "application/json", Bytes: []byte("{}\n"), }, }, }, nil } type warningOutputEncoder struct{} func (warningOutputEncoder) Key() string { return "json" } func (warningOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) { result, err := jsonoutput.New().Encode(ctx, req) if err != nil { return contracts.OutputResult{}, err } result.Warnings = append(result.Warnings, contracts.Warning{ Scope: "output", ReasonCode: "synthetic_warning", Message: "synthetic output warning", }) return result, nil } func registriesWithOutput(t *testing.T, encoder contracts.OutputEncoder) pipeline.Registries { t.Helper() registries, err := productionRegistries() if err != nil { t.Fatalf("productionRegistries: %v", err) } outputs := pipeline.NewOutputEncoderRegistry() if err := outputs.RegisterWithSpec(jsonoutput.ModuleSpec(), func() (contracts.OutputEncoder, error) { return encoder, nil }); err != nil { t.Fatalf("register test output encoder: %v", err) } registries.Outputs = outputs return registries } func onlyChildDir(t *testing.T, root string) string { t.Helper() children := childDirs(t, root) if len(children) != 1 { t.Fatalf("child dirs under %q = %v, want one", root, children) } return children[0] } func childDirs(t *testing.T, root string) []string { t.Helper() entries, err := os.ReadDir(root) if err != nil { if os.IsNotExist(err) { return nil } t.Fatalf("read dir %q: %v", root, err) } var dirs []string for _, entry := range entries { if entry.IsDir() { dirs = append(dirs, filepath.Join(root, entry.Name())) } } return dirs } func readFile(t *testing.T, path string) []byte { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read %q: %v", path, err) } return data } func readJSONFile(t *testing.T, path string, out any) { t.Helper() if err := json.Unmarshal(readFile(t, path), out); err != nil { t.Fatalf("unmarshal %q: %v", path, err) } } func assertNoTemporaryFiles(t *testing.T, root string) { t.Helper() if err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { if err != nil { return err } if strings.Contains(entry.Name(), ".tmp-") { t.Fatalf("temporary file remains after success: %s", path) } return nil }); err != nil { t.Fatalf("walk output dir %q: %v", root, err) } } func fakeCatalog(t *testing.T) pipeline.ModuleCatalog { t.Helper() inputs := pipeline.NewInputAdapterRegistry() chunkers := pipeline.NewChunkerRegistry() extractors := pipeline.NewExtractorRegistry() mergers := pipeline.NewMergerRegistry() normalizers := pipeline.NewNormalizerRegistry() validators := pipeline.NewValidatorRegistry() outputs := pipeline.NewOutputEncoderRegistry() mustRegisterInput(t, inputs, pipeline.ModuleSpec{Key: "fake/input", Stage: pipeline.StageInput, Provides: []string{"source"}}) mustRegisterChunker(t, chunkers, pipeline.ModuleSpec{Key: "generic", Stage: pipeline.StageChunk, Requires: []string{"source"}, Provides: []string{"chunks"}}) mustRegisterExtractor(t, extractors, pipeline.ModuleSpec{Key: "fake/extract", Stage: pipeline.StageExtract, Requires: []string{"chunks"}, Provides: []string{"artifact"}}) mustRegisterMerger(t, mergers, pipeline.ModuleSpec{Key: "appendorder", Stage: pipeline.StageMerge, Requires: []string{"artifact"}, Provides: []string{"merged"}}) mustRegisterNormalizer(t, normalizers, pipeline.ModuleSpec{Key: "noop", Stage: pipeline.StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}}) mustRegisterOutput(t, outputs, pipeline.ModuleSpec{Key: "json", Stage: pipeline.StageOutput, Requires: []string{"normalized"}}) return pipeline.ModuleCatalog{ Inputs: inputs, Chunkers: chunkers, Extractors: extractors, Mergers: mergers, Normalizers: normalizers, Validators: validators, Outputs: outputs, } } func mustRegisterInput(t *testing.T, registry *pipeline.InputAdapterRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.InputAdapter, error) { return nil, nil }); err != nil { t.Fatalf("register input: %v", err) } } func mustRegisterChunker(t *testing.T, registry *pipeline.ChunkerRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.Chunker, error) { return nil, nil }); err != nil { t.Fatalf("register chunker: %v", err) } } func mustRegisterExtractor(t *testing.T, registry *pipeline.ExtractorRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.Extractor, error) { return nil, nil }); err != nil { t.Fatalf("register extractor: %v", err) } } func mustRegisterMerger(t *testing.T, registry *pipeline.MergerRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.Merger, error) { return nil, nil }); err != nil { t.Fatalf("register merger: %v", err) } } func mustRegisterNormalizer(t *testing.T, registry *pipeline.NormalizerRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.Normalizer, error) { return nil, nil }); err != nil { t.Fatalf("register normalizer: %v", err) } } func mustRegisterOutput(t *testing.T, registry *pipeline.OutputEncoderRegistry, spec pipeline.ModuleSpec) { t.Helper() if err := registry.RegisterWithSpec(spec, func() (contracts.OutputEncoder, error) { return nil, nil }); err != nil { t.Fatalf("register output: %v", err) } } func mapLookup(values map[string]string) func(string) (string, bool) { return func(key string) (string, bool) { value, ok := values[key] return value, ok } }