package artifacts import ( "errors" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestNormalizeSessionArtifactSource(t *testing.T) { tests := []struct { name string source string wantID string wantErr string }{ {name: "legacy alias processed unsupported", source: "processed_transcript", wantErr: "unsupported artifact source"}, {name: "legacy alias normalized unsupported", source: "normalized_transcript", wantErr: "unsupported artifact source"}, {name: "legacy alias trimmed unsupported", source: "trimmed_transcript", wantErr: "unsupported artifact source"}, {name: "canonical", source: ArtifactTranscriptTrimmed, wantID: ArtifactTranscriptTrimmed}, {name: "unsupported", source: "narratio.unknown", wantErr: "unsupported artifact source"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := NormalizeSessionArtifactSource(tt.source) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("NormalizeSessionArtifactSource() error = %v, want contains %q", err, tt.wantErr) } return } if err != nil { t.Fatalf("NormalizeSessionArtifactSource() error = %v", err) } if got != tt.wantID { t.Fatalf("NormalizeSessionArtifactSource() = %q, want %q", got, tt.wantID) } }) } } func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") if err := os.MkdirAll(paths.ArtifactsDir, 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json") if err := os.WriteFile(manifestPath, []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } canonicalPath := filepath.Join(paths.TranscriptsDir, "normalized.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(canonicalPath, []byte(`{"segments":[{"id":123}]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } m := manifest.New("session", time.Now().UTC()) m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ {Kind: "transcript_normalized", LocalPath: manifestPath, ProducerRunID: "run-123"}, }) resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFull) if err != nil { t.Fatalf("ResolveSessionArtifact() error = %v", err) } if resolved.Path != manifestPath { t.Fatalf("resolved path = %q, want %q", resolved.Path, manifestPath) } if resolved.Provenance != "manifest.normalize.outputs" { t.Fatalf("provenance = %q, want manifest.normalize.outputs", resolved.Provenance) } if resolved.ProducerRunID != "run-123" { t.Fatalf("producer run id = %q, want run-123", resolved.ProducerRunID) } } func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(canonicalPath, []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) if err != nil { t.Fatalf("ResolveSessionArtifact() error = %v", err) } if resolved.Path != canonicalPath { t.Fatalf("resolved path = %q, want %q", resolved.Path, canonicalPath) } if resolved.Provenance != "fallback.canonical_path" { t.Fatalf("provenance = %q, want fallback.canonical_path", resolved.Provenance) } } func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") _, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) if err == nil { t.Fatal("expected error, got nil") } if !errors.Is(err, ErrSessionArtifactNotFound) { t.Fatalf("errors.Is(err, ErrSessionArtifactNotFound) = false; err=%v", err) } } func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") canonicalPath := filepath.Join(paths.TranscriptsDir, "processed.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(canonicalPath, []byte(`{"not_segments":[]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } _, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptPolished) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "top-level segments is required") { t.Fatalf("error = %q, want segments validation error", err.Error()) } } func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(canonicalPath, []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptTrimmed, NewArtifactCatalog()) if err != nil { t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err) } if resolved.Path != canonicalPath { t.Fatalf("resolved.Path = %q, want %q", resolved.Path, canonicalPath) } if resolved.Provenance != "fallback.canonical_path" { t.Fatalf("provenance = %q, want fallback.canonical_path", resolved.Provenance) } } func TestResolveSessionArtifactWithCatalogConfiguredAvailableGenerated(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") outputPath := filepath.Join(paths.ArtifactsDir, "session_recap.md") if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(outputPath, []byte("recap\n"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } catalog := NewArtifactCatalog() if err := catalog.RegisterConfiguredArtifacts( map[string]ConfiguredArtifactDefinition{ "session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"}, }, nil, ); err != nil { t.Fatalf("RegisterConfiguredArtifacts() error = %v", err) } sourceID := ConfiguredArtifactSourceID("session_recap") if err := catalog.MarkAvailableGenerated(sourceID, outputPath); err != nil { t.Fatalf("MarkAvailableGenerated() error = %v", err) } resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, sourceID, catalog) if err != nil { t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err) } if resolved.Path != outputPath { t.Fatalf("resolved.Path = %q, want %q", resolved.Path, outputPath) } if resolved.Provenance != ArtifactProvenanceGeneratedCurrentAnalyzeRun { t.Fatalf("provenance = %q, want %q", resolved.Provenance, ArtifactProvenanceGeneratedCurrentAnalyzeRun) } } func TestResolveSessionArtifactWithCatalogConfiguredAvailableFromDisk(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") outputPath := filepath.Join(paths.ArtifactsDir, "player_handout.md") if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(outputPath, []byte("handout\n"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } catalog := NewArtifactCatalog() if err := catalog.RegisterConfiguredArtifacts( map[string]ConfiguredArtifactDefinition{ "player_handout": {Enabled: false, OutputPath: "artifacts/player_handout.md"}, }, nil, ); err != nil { t.Fatalf("RegisterConfiguredArtifacts() error = %v", err) } sourceID := ConfiguredArtifactSourceID("player_handout") if err := catalog.MarkAvailableFromDisk(sourceID, outputPath); err != nil { t.Fatalf("MarkAvailableFromDisk() error = %v", err) } resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, sourceID, catalog) if err != nil { t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err) } if resolved.Provenance != ArtifactProvenanceDisabledFromDisk { t.Fatalf("provenance = %q, want %q", resolved.Provenance, ArtifactProvenanceDisabledFromDisk) } } func TestResolveSessionArtifactWithCatalogConfiguredPlannedButUnavailable(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") catalog := NewArtifactCatalog() if err := catalog.RegisterConfiguredArtifacts( map[string]ConfiguredArtifactDefinition{ "session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"}, }, nil, ); err != nil { t.Fatalf("RegisterConfiguredArtifacts() error = %v", err) } _, err := ResolveSessionArtifactWithCatalog(paths, nil, ConfiguredArtifactSourceID("session_recap"), catalog) if err == nil { t.Fatal("expected error, got nil") } if !errors.Is(err, ErrSessionArtifactNotFound) { t.Fatalf("errors.Is(err, ErrSessionArtifactNotFound)=false; err=%v", err) } } func TestResolveSessionArtifactWithCatalogUnsupportedConfiguredSourceFails(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") catalog := NewArtifactCatalog() _, err := ResolveSessionArtifactWithCatalog(paths, nil, "narratio.artifact.unknown", catalog) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "unsupported artifact source") { t.Fatalf("error = %q, want unsupported artifact source", err.Error()) } }