package app import ( "bytes" "context" "errors" "path/filepath" "reflect" "strings" "testing" ) const artifactCatalogHeader = "key\tsource\tdefinitions\tenabled\toutput\tdependencies\tfamily\tcharacter\tfamily_source\tparty_source\n" func TestConfigArtifactsReportsOrdinaryCatalogWithoutCampaign(t *testing.T) { dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") mustWriteTestFile(t, pipelinePath, `workspace: root: `+filepath.Join(dir, "workspace")+` whisperx: transcribe_url: https://transcription.example.com/transcribe scriptorium: binary: scriptorium artifacts: zeta: enabled: false output_path: artifacts/zeta.md alpha: enabled: false output_path: artifacts/alpha.md inputs: upstream: {source: narratio.artifact.zeta, required: true} `) var out bytes.Buffer if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath}, &out); err != nil { t.Fatalf("ConfigArtifacts() error = %v", err) } lines := strings.Split(strings.TrimSuffix(out.String(), "\n"), "\n") if len(lines) != 3 || !strings.HasPrefix(lines[1], "alpha\t") || !strings.HasPrefix(lines[2], "zeta\t") { t.Fatalf("catalog order/output = %q", out.String()) } alpha := strings.Split(lines[1], "\t") if len(alpha) != 10 { t.Fatalf("alpha fields = %#v", alpha) } if alpha[1] != "narratio.artifact.alpha" || alpha[2] != pipelinePath || alpha[3] != "false" || alpha[4] != "artifacts/alpha.md" || alpha[5] != "zeta" { t.Fatalf("alpha record = %#v", alpha) } } func TestConfigArtifactsEmptyCatalogWritesOnlyHeader(t *testing.T) { dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") mustWriteTestFile(t, pipelinePath, "workspace:\n root: "+filepath.Join(dir, "workspace")+"\nwhisperx:\n transcribe_url: https://transcription.example.com/transcribe\n") var out bytes.Buffer if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath}, &out); err != nil { t.Fatal(err) } if out.String() != artifactCatalogHeader { t.Fatalf("empty catalog = %q, want header only", out.String()) } } func TestConfigArtifactsReportsGeneratedFamilyProvenance(t *testing.T) { pipelinePath, campaignPath := writeInspectionFamilyConfig(t) var out bytes.Buffer if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &out); err != nil { t.Fatal(err) } records := catalogRecordsByKey(t, out.String()) meta := records["character_meta_arannis"] note := records["character_note_arannis"] partyPath := filepath.Join(filepath.Dir(campaignPath), "party.yml") if len(meta) != 10 || meta[6] != "character_meta" || meta[7] != "arannis" || meta[8] != pipelinePath || meta[9] != partyPath { t.Fatalf("generated meta record = %#v", meta) } if len(note) != 10 || note[5] != "character_meta_arannis" || note[6] != "character_note" { t.Fatalf("generated note record = %#v", note) } } func TestConfigArtifactsReportsOverlayFamilyContributors(t *testing.T) { pipelinePath, campaignPath := writeInspectionFamilyConfig(t) overlayPath := filepath.Join(filepath.Dir(pipelinePath), "overlays", "testing.yml") mustWriteTestFile(t, overlayPath, `scriptorium: artifact_families: character_note: prompt_id: dnd.character_note.testing `) args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--overlay", "testing"} var catalog bytes.Buffer if err := ConfigArtifacts(context.Background(), args, &catalog); err != nil { t.Fatal(err) } records := catalogRecordsByKey(t, catalog.String()) if got := strings.Split(records["character_note_arannis"][2], ","); !reflect.DeepEqual(got, []string{overlayPath, pipelinePath}) { t.Fatalf("generated family definitions = %#v", got) } var sources bytes.Buffer if err := ConfigSources(context.Background(), args, &sources); err != nil { t.Fatal(err) } want := "scriptorium.artifacts.character_note_arannis.prompt_id\tfamily\t" + overlayPath if !strings.Contains(sources.String(), want) { t.Fatalf("sources output missing overlay family contributor %q:\n%s", want, sources.String()) } } func TestConfigArtifactsReportsBaseAndOverlayContributors(t *testing.T) { dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") overlayPath := filepath.Join(dir, "overlays", "testing.yml") mustWriteTestFile(t, pipelinePath, `workspace: root: /srv/narratio whisperx: transcribe_url: https://transcription.example.com/transcribe scriptorium: binary: scriptorium artifacts: notes: enabled: false output_path: artifacts/notes.md `) mustWriteTestFile(t, overlayPath, `scriptorium: artifacts: notes: enabled: true prompt_id: notes overlay_only: enabled: false output_path: artifacts/overlay-only.md `) var out bytes.Buffer if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--overlay", "testing"}, &out); err != nil { t.Fatal(err) } records := catalogRecordsByKey(t, out.String()) if got := strings.Split(records["notes"][2], ","); !reflect.DeepEqual(got, []string{overlayPath, pipelinePath}) { t.Fatalf("notes definitions = %#v", got) } if got := records["overlay_only"][2]; got != overlayPath { t.Fatalf("overlay-only definition = %q, want %q", got, overlayPath) } } func TestConfigArtifactsIdentityDoesNotDependOnDefinitionFilename(t *testing.T) { inlineDir := t.TempDir() inlinePath := filepath.Join(inlineDir, "pipeline.yml") mustWriteTestFile(t, inlinePath, artifactIdentityPipeline("", "recap")) importDir := t.TempDir() importPath := filepath.Join(importDir, "pipeline.yml") mustWriteTestFile(t, importPath, artifactIdentityPipeline("artifacts.yml", "")) mustWriteTestFile(t, filepath.Join(importDir, "artifacts.yml"), "scriptorium:\n artifacts:\n recap:\n enabled: false\n output_path: artifacts/recap.md\n") for _, path := range []string{inlinePath, importPath} { var out bytes.Buffer if err := ConfigArtifacts(context.Background(), []string{"--config", path}, &out); err != nil { t.Fatal(err) } if got := catalogRecordsByKey(t, out.String())["recap"][1]; got != "narratio.artifact.recap" { t.Fatalf("source identity for %s = %q", path, got) } } } func TestConfigArtifactsPropagatesWriterErrorsAndEscapesFields(t *testing.T) { dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") mustWriteTestFile(t, pipelinePath, "workspace:\n root: /srv/narratio\nwhisperx:\n transcribe_url: https://transcription.example.com/transcribe\n") want := errors.New("write rejected") err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath}, rejectingCatalogWriter{err: want}) if !errors.Is(err, want) { t.Fatalf("ConfigArtifacts() error = %v, want writer error", err) } if got := escapeCatalogField("a\\b\tc\nd\r"); got != `a\\b\tc\nd\r` { t.Fatalf("escaped field = %q", got) } } func catalogRecordsByKey(t *testing.T, output string) map[string][]string { t.Helper() lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") if len(lines) == 0 || lines[0]+"\n" != artifactCatalogHeader { t.Fatalf("catalog header = %q", output) } records := make(map[string][]string, len(lines)-1) for _, line := range lines[1:] { fields := strings.Split(line, "\t") records[fields[0]] = fields } return records } func artifactIdentityPipeline(importPath, inlineKey string) string { document := "workspace:\n root: /srv/narratio\nwhisperx:\n transcribe_url: https://transcription.example.com/transcribe\nscriptorium:\n binary: scriptorium\n" if importPath != "" { document = "configkit:\n imports:\n - path: " + importPath + "\n" + document } if inlineKey != "" { document += " artifacts:\n " + inlineKey + ":\n enabled: false\n output_path: artifacts/recap.md\n" } return document } type rejectingCatalogWriter struct{ err error } func (w rejectingCatalogWriter) Write([]byte) (int, error) { return 0, w.err }