148 lines
5.3 KiB
Go
148 lines
5.3 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/stage"
|
|
)
|
|
|
|
// executeStages keeps runner tests focused on controlled stage doubles. The
|
|
// production command path always supplies one validated BoundedPlan directly.
|
|
func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage, opts RunOptions) (*RunSummary, error) {
|
|
plan := BoundedPlan{stages: append([]stage.Stage(nil), stages...)}
|
|
return executePlan(ctx, cfg, plan, opts)
|
|
}
|
|
|
|
func seedCurrentSemanticEvidence(t *testing.T, cfg *config.Config, m *manifest.Manifest, names ...string) {
|
|
t.Helper()
|
|
providers := make(map[string]stage.SemanticConfigFingerprinter)
|
|
for _, candidate := range stage.All() {
|
|
if provider, ok := candidate.(stage.SemanticConfigFingerprinter); ok {
|
|
providers[candidate.Name()] = provider
|
|
}
|
|
}
|
|
for _, name := range names {
|
|
record := m.Stages[name]
|
|
if record == nil {
|
|
t.Fatalf("stage %q must exist before semantic evidence is seeded", name)
|
|
}
|
|
provider, ok := providers[name]
|
|
if !ok {
|
|
t.Fatalf("stage %q has no semantic fingerprint provider", name)
|
|
}
|
|
fingerprint, err := provider.SemanticConfigFingerprint(&stage.Env{Config: cfg})
|
|
if err != nil {
|
|
t.Fatalf("fingerprint stage %q: %v", name, err)
|
|
}
|
|
record.SemanticConfig = &fingerprint
|
|
if name == "transcribe" {
|
|
seedCurrentTranscribeEvidence(t, cfg, m, fingerprint)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedCurrentTranscribeEvidence(
|
|
t *testing.T,
|
|
cfg *config.Config,
|
|
m *manifest.Manifest,
|
|
semantic manifest.SemanticConfigFingerprint,
|
|
) {
|
|
t.Helper()
|
|
store := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
|
|
paths := store.SessionPathsFor(cfg.Session.Campaign, m.SessionID)
|
|
if strings.TrimSpace(m.RunID) == "" {
|
|
m.RunID = "20260503T010203Z-a1b2c3d4"
|
|
}
|
|
records := make(map[string]manifest.TranscribeSpeakerRecord)
|
|
for _, input := range m.Inputs {
|
|
if input.Kind != "audio" {
|
|
continue
|
|
}
|
|
audioPath := artifacts.ResolveSessionLocalPathForRead(paths, input.Path)
|
|
audioBytes, err := os.ReadFile(audioPath)
|
|
if err != nil {
|
|
t.Fatalf("read transcribe audio fixture %q: %v", audioPath, err)
|
|
}
|
|
audioRelative, err := pathsafe.SlashRelativeFromRoot(paths.Root, audioPath)
|
|
if err != nil {
|
|
t.Fatalf("relativize transcribe audio fixture %q: %v", audioPath, err)
|
|
}
|
|
speaker := strings.TrimSuffix(filepath.Base(audioPath), filepath.Ext(audioPath))
|
|
outputRelative := filepath.ToSlash(filepath.Join("transcripts", "raw", speaker+".json"))
|
|
outputPath, err := pathsafe.JoinSlashRelativeUnderRoot(paths.Root, outputRelative)
|
|
if err != nil {
|
|
t.Fatalf("resolve transcribe output fixture: %v", err)
|
|
}
|
|
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
|
|
mustWriteTestFile(t, outputPath, `{"segments":[]}`)
|
|
} else if err != nil {
|
|
t.Fatalf("inspect transcribe output fixture %q: %v", outputPath, err)
|
|
}
|
|
outputBytes, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
t.Fatalf("read transcribe output fixture %q: %v", outputPath, err)
|
|
}
|
|
audioDigest := sha256.Sum256(audioBytes)
|
|
outputDigest := sha256.Sum256(outputBytes)
|
|
fingerprint, err := stage.FingerprintSemanticConfig(
|
|
manifest.TranscribeFingerprintContractVersion,
|
|
struct {
|
|
SpeakerID string `json:"speaker_id"`
|
|
AudioPath string `json:"audio_path"`
|
|
AudioSHA256 string `json:"audio_sha256"`
|
|
AudioSize int64 `json:"audio_size"`
|
|
SemanticConfigVersion int `json:"semantic_config_version"`
|
|
SemanticConfigDigest string `json:"semantic_config_digest"`
|
|
}{
|
|
SpeakerID: speaker, AudioPath: audioRelative,
|
|
AudioSHA256: hex.EncodeToString(audioDigest[:]), AudioSize: int64(len(audioBytes)),
|
|
SemanticConfigVersion: semantic.Version, SemanticConfigDigest: semantic.Digest,
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("fingerprint transcribe speaker fixture %q: %v", speaker, err)
|
|
}
|
|
output := manifest.ArtifactRecord{
|
|
Kind: "transcript_raw", LocalPath: outputRelative,
|
|
ProducerRunID: m.RunID, Checksum: hex.EncodeToString(outputDigest[:]),
|
|
}
|
|
records[speaker] = manifest.TranscribeSpeakerRecord{
|
|
FingerprintVersion: manifest.TranscribeFingerprintContractVersion,
|
|
Fingerprint: fingerprint.Digest, AudioPath: audioRelative,
|
|
AudioSHA256: hex.EncodeToString(audioDigest[:]), AudioSize: int64(len(audioBytes)),
|
|
Output: output, OutputSize: int64(len(outputBytes)), ProducerRunID: m.RunID,
|
|
}
|
|
}
|
|
record := m.Stages["transcribe"]
|
|
record.TranscribeStateVersion = manifest.TranscribeStateContractVersion
|
|
record.TranscribeSpeakers = records
|
|
if len(record.Outputs) == 0 {
|
|
for _, speakerRecord := range records {
|
|
record.Outputs = append(record.Outputs, speakerRecord.Output)
|
|
}
|
|
slices.SortFunc(record.Outputs, func(a, b manifest.ArtifactRecord) int {
|
|
return strings.Compare(a.LocalPath, b.LocalPath)
|
|
})
|
|
}
|
|
}
|
|
|
|
func loadConfigForSemanticEvidence(t *testing.T, pipelinePath, campaignPath, sessionPath string) *config.Config {
|
|
t.Helper()
|
|
cfg, err := config.Load(pipelinePath, campaignPath, sessionPath)
|
|
if err != nil {
|
|
t.Fatalf("load config for semantic evidence: %v", err)
|
|
}
|
|
return cfg
|
|
}
|