package app import ( "os" "path/filepath" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/narratio/internal/config" ) func TestResolveEffectiveArtifactsExpandsFamilySelections(t *testing.T) { dir := t.TempDir() write := func(name, body string) string { path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } pipeline := write("pipeline.yml", `workspace: {root: /tmp/narratio-work} whisperx: {transcribe_url: https://example.test/transcribe} notification: {mode: noop} scriptorium: artifact_families: character_meta: enabled: false for_each: party.characters prompt_id: dnd.character_meta output_path_pattern: artifacts/characters/{character_id}/meta.md `) campaign := write("campaign.yml", `campaign_id: campaign inputs: {speakers_file: speakers.yml, autocorrect_file: autocorrect.yml, glossary_file: glossary.yml, party_file: party.yml} `) session := write("session.yml", `session_id: session campaign: campaign inputs: {audio_dir: audio} `) write("party.yml", `schema_version: narratio.party.v1 characters: zeta: {player: {name: Z}, character: {name: Zeta, classes: [{name: wizard}]}} alpha: {player: {name: A}, character: {name: Alpha, classes: [{name: ranger}]}} `) cfg, err := config.LoadWithSessionOptions(pipeline, campaign, session, config.SessionLoadOptions{}) if err != nil { t.Fatal(err) } effective, err := resolveEffectiveArtifacts(cfg, []string{"character_meta", "character_meta_alpha"}) if err != nil { t.Fatal(err) } if got, want := effective.Keys(), []string{"character_meta_alpha", "character_meta_zeta"}; !reflect.DeepEqual(got, want) { t.Fatalf("keys = %#v, want %#v", got, want) } if origin, ok := effective.Origin("character_meta_alpha"); !ok || origin.Family != "character_meta" || origin.CharacterID != "alpha" { t.Fatalf("origin = %#v, %t", origin, ok) } if _, err := resolveEffectiveArtifacts(cfg, []string{"unknown"}); err == nil || !strings.Contains(err.Error(), "unknown artifact") { t.Fatalf("unknown selection error = %v", err) } if defaultEffective, err := resolveEffectiveArtifacts(cfg, nil); err != nil { t.Fatal(err) } else if len(defaultEffective.Keys()) != 0 { t.Fatal("default selection should omit disabled family members") } } func TestArtifactSelectionFlagNormalize(t *testing.T) { tests := []struct { name string inputs []string want []string wantErr string }{ { name: "single value", inputs: []string{"session_recap"}, want: []string{"session_recap"}, }, { name: "repeatable and comma separated values are deduped and sorted", inputs: []string{"session_recap,player_handout", "session_recap"}, want: []string{"player_handout", "session_recap"}, }, { name: "empty token fails", inputs: []string{"session_recap,"}, wantErr: "artifact names must be non-empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var flag artifactSelectionFlag for _, in := range tt.inputs { if err := flag.Set(in); err != nil { t.Fatalf("Set(%q) error = %v", in, err) } } got, err := flag.Normalize() if tt.wantErr != "" { if err == nil { t.Fatalf("Normalize() error = nil, want %q", tt.wantErr) } if err.Error() != tt.wantErr { t.Fatalf("Normalize() error = %q, want %q", err.Error(), tt.wantErr) } return } if err != nil { t.Fatalf("Normalize() error = %v", err) } if len(got) != len(tt.want) { t.Fatalf("Normalize() len = %d, want %d; got=%v", len(got), len(tt.want), got) } for i := range got { if got[i] != tt.want[i] { t.Fatalf("Normalize()[%d] = %q, want %q", i, got[i], tt.want[i]) } } }) } } func TestValidateSelectedArtifacts(t *testing.T) { tests := []struct { name string cfg *config.Config selected []string wantErr string }{ { name: "empty selection is accepted", cfg: &config.Config{}, selected: nil, }, { name: "scriptorium required when selected artifacts present", cfg: &config.Config{Pipeline: &config.PipelineConfig{}}, selected: []string{"session_recap"}, wantErr: "--artifacts requires pipeline.scriptorium.artifacts to be configured", }, { name: "unknown selected artifact fails", cfg: &config.Config{ Pipeline: &config.PipelineConfig{ Scriptorium: &config.ScriptoriumConfig{ Artifacts: map[string]config.ScriptoriumArtifactConfig{ "session_recap": {Enabled: true, PromptID: "dnd.session_recap", OutputPath: "artifacts/session_recap.md"}, }, }, }, }, selected: []string{"player_handout"}, wantErr: `--artifacts includes unknown artifact "player_handout"`, }, { name: "known selected artifacts are accepted", cfg: &config.Config{ Pipeline: &config.PipelineConfig{ Scriptorium: &config.ScriptoriumConfig{ Artifacts: map[string]config.ScriptoriumArtifactConfig{ "session_recap": {Enabled: true, PromptID: "dnd.session_recap", OutputPath: "artifacts/session_recap.md"}, "player_handout": {Enabled: true, PromptID: "dnd.player_handout", OutputPath: "artifacts/player_handout.md"}, }, }, }, }, selected: []string{"player_handout", "session_recap"}, }, { name: "selected disabled artifact must be executable", cfg: &config.Config{ Pipeline: &config.PipelineConfig{ Scriptorium: &config.ScriptoriumConfig{ Artifacts: map[string]config.ScriptoriumArtifactConfig{ "player_handout": {Enabled: false, OutputPath: "artifacts/player_handout.md"}, }, }, }, }, selected: []string{"player_handout"}, wantErr: "pipeline.scriptorium.artifacts.player_handout.prompt_id is required when selected", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateSelectedArtifacts(tt.cfg, tt.selected) if tt.wantErr != "" { if err == nil { t.Fatalf("error = nil, want %q", tt.wantErr) } if err.Error() != tt.wantErr { t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr) } return } if err != nil { t.Fatalf("error = %v, want nil", err) } }) } }