Require manifest-backed local artifacts
This commit is contained in:
@@ -20,8 +20,8 @@ The internal registry recognizes these stable built-in source IDs:
|
||||
- `narratio.transcript.final_trimmed_markdown`
|
||||
- `narratio.bounds.session`
|
||||
|
||||
Registry entries bind each ID to its producer, output kind, canonical fallback,
|
||||
and content validator. The focused stage documents own their input/output flow;
|
||||
Registry entries bind each ID to its producer, output kind, and content
|
||||
validator. The focused stage documents own their input/output flow;
|
||||
[Configuration](../config.md) owns where operators may select these IDs.
|
||||
|
||||
## Configured, Extraction, And Previous-Session Sources
|
||||
@@ -91,8 +91,13 @@ Current provenance values:
|
||||
|
||||
Built-ins:
|
||||
|
||||
1. manifest producer outputs (when present)
|
||||
2. canonical session-path fallback
|
||||
- resolve only from the current manifest's succeeded registered producer;
|
||||
- require exactly one output with the registered source ID and output kind, a
|
||||
safe session-rooted path, producer run ID, canonical checksum, and valid
|
||||
content; and
|
||||
- verify the registered checksum against the durable bytes. Incidental files,
|
||||
canonical filenames, duplicate outputs, failed/stale producer records, and
|
||||
malformed evidence are unavailable.
|
||||
|
||||
Configured sources (`narratio.artifact.*`):
|
||||
|
||||
|
||||
@@ -41,10 +41,9 @@ Exact remote placement and the operator workflow belong in
|
||||
- resolves publish output sources through runtime artifact catalog and
|
||||
manifest-aware resolution. Configured Scriptorium outputs are publishable
|
||||
only from validated `current` per-artifact analyze evidence; an incidental
|
||||
canonical file, legacy aggregate output, stale/failed/unselected record, or
|
||||
mismatched path, size, or checksum remains unavailable. This does not change
|
||||
the explicit compatibility policies owned by built-in, extraction, or
|
||||
previous-session sources.
|
||||
canonical file, aggregate output, stale/failed/unselected record, or
|
||||
mismatched path, size, or checksum remains unavailable. Built-in and
|
||||
extraction sources likewise require their own current manifest evidence.
|
||||
- publishes extraction lanes only through explicit configured output rules;
|
||||
neither run-local nor durable Notarius bundles are scanned or uploaded wholesale.
|
||||
- selected artifact filter applies to configured artifact sources only.
|
||||
|
||||
@@ -102,8 +102,7 @@ Canonical stage order:
|
||||
Execution rules:
|
||||
|
||||
- succeeded stages are skipped unless `--force` is set; stages with semantic
|
||||
configuration contracts additionally require matching versioned evidence,
|
||||
and missing legacy evidence causes a safe one-time rerun;
|
||||
configuration contracts additionally require matching versioned evidence;
|
||||
- `run` continues interrupted or partially completed sessions by running non-succeeded stages;
|
||||
- forcing a stage marks succeeded transitive dependents as `stale` before the
|
||||
replacement runs; render and extract are independent siblings; and
|
||||
|
||||
@@ -317,7 +317,7 @@ path candidates or remote probing.
|
||||
|
||||
## Stage 6 — Make Local Artifact Evidence Manifest-Only
|
||||
|
||||
**Status: Pending**
|
||||
**Status: Completed**
|
||||
|
||||
### Goal
|
||||
|
||||
|
||||
@@ -428,6 +428,25 @@ narratio analyze 2026-04-04 --artifacts session_recap
|
||||
Relevant references: [Operations: Artifact Selection](./operations.md#artifact-selection)
|
||||
and [Artifact Internals](./internal/artifacts.md#resolution-rules).
|
||||
|
||||
## Built-in transcript or bounds source is unavailable
|
||||
|
||||
Symptom:
|
||||
|
||||
- analyze, extract, render, or publish says a built-in source is unavailable
|
||||
even though a file exists at its usual session path.
|
||||
|
||||
Likely cause:
|
||||
|
||||
- the producing stage has not succeeded, its output lacks the exact registered
|
||||
source, run ID, or checksum, its evidence is duplicated or stale, or the
|
||||
durable bytes no longer match the manifest checksum.
|
||||
|
||||
Safe fix:
|
||||
|
||||
- do not copy or rename the file to make it discoverable. Rerun the producing
|
||||
stage to regenerate both the bytes and current manifest evidence, then retry
|
||||
the consumer.
|
||||
|
||||
## Legacy aggregate analysis requires regeneration
|
||||
|
||||
Symptom:
|
||||
|
||||
31
internal/app/artifact_evidence_test.go
Normal file
31
internal/app/artifact_evidence_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
func setCurrentBuiltInEvidence(t *testing.T, m *manifest.Manifest, stage, kind, sourceID, path string) {
|
||||
t.Helper()
|
||||
checksum, err := artifacts.SHA256File(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.MarkStageSucceeded(stage, time.Date(2026, 5, 16, 1, 2, 3, 0, time.UTC), []manifest.ArtifactRecord{{
|
||||
Kind: kind, SourceID: sourceID, LocalPath: path, ProducerRunID: stage + "-run", Checksum: checksum,
|
||||
}})
|
||||
}
|
||||
|
||||
func seedCurrentBuiltInEvidence(t *testing.T, manifestPath, sessionID, campaign, stage, kind, sourceID, path string) {
|
||||
t.Helper()
|
||||
m := manifest.New(sessionID, time.Now().UTC())
|
||||
m.Campaign = campaign
|
||||
setCurrentBuiltInEvidence(t, m, stage, kind, sourceID, path)
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), manifestPath, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
@@ -126,7 +127,9 @@ func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
polishedPath := filepath.Join(workRoot, "transcripts", "polished.json")
|
||||
mustWriteTestFile(t, polishedPath, `{"segments":[{"id":1}]}`)
|
||||
seedCurrentBuiltInEvidence(t, filepath.Join(workRoot, "manifest.json"), "2026-05-03", "sample-campaign", "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, polishedPath)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -247,7 +250,9 @@ inputs:
|
||||
})
|
||||
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"schema":"seriatim-intermediate","segments":[]}`)
|
||||
basePath := filepath.Join(workRoot, "transcripts", "base.json")
|
||||
mustWriteTestFile(t, basePath, `{"schema":"seriatim-intermediate","segments":[]}`)
|
||||
seedCurrentBuiltInEvidence(t, filepath.Join(workRoot, "manifest.json"), sessionID, "sample-campaign", "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
||||
@@ -156,7 +156,7 @@ func TestExtractLifecyclePrepareBindsVerifiedReferenceSnapshots(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("executeStages() error = %v", err)
|
||||
}
|
||||
if len(summary.Executed) != 2 || len(runner.requests) != 1 {
|
||||
if len(summary.Executed) != 3 || len(runner.requests) != 1 {
|
||||
t.Fatalf("summary = %#v requests=%d", summary, len(runner.requests))
|
||||
}
|
||||
paths, err := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
@@ -510,12 +510,12 @@ func TestExtractLifecycleRerunsAfterDirectTranscriptChange(t *testing.T) {
|
||||
t.Fatalf("Save(mutated) error = %v", err)
|
||||
}
|
||||
|
||||
rerun, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env})
|
||||
if err != nil {
|
||||
t.Fatalf("rerun executeStages() error = %v", err)
|
||||
_, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env})
|
||||
if err == nil || !strings.Contains(err.Error(), "checksum mismatch") {
|
||||
t.Fatalf("rerun executeStages() error = %v, want stale transcript evidence rejection", err)
|
||||
}
|
||||
if len(rerun.Executed) != 2 || len(rerun.Skipped) != 0 || len(runner.requests) != 2 || analyzeRuns != 2 {
|
||||
t.Fatalf("rerun summary = %#v requests=%d analyze=%d", rerun, len(runner.requests), analyzeRuns)
|
||||
if len(runner.requests) != 1 || analyzeRuns != 1 {
|
||||
t.Fatalf("stale transcript should prevent extraction, requests=%d analyze=%d", len(runner.requests), analyzeRuns)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +645,24 @@ func prepareExtractLifecyclePlan(t *testing.T) []stage.Stage {
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSingleStagePlan(extract) error = %v", err)
|
||||
}
|
||||
return append(prepare, extract...)
|
||||
return append(append(prepare, lifecycleTrimEvidenceStage{}), extract...)
|
||||
}
|
||||
|
||||
type lifecycleTrimEvidenceStage struct{}
|
||||
|
||||
func (lifecycleTrimEvidenceStage) Name() string { return "trim" }
|
||||
|
||||
func (lifecycleTrimEvidenceStage) Run(_ context.Context, env *stage.Env, m *manifest.Manifest) (*stage.StageResult, error) {
|
||||
paths := artifacts.NewLocalStore(env.Config.Pipeline.Workspace.Root).SessionPathsFor(m.Campaign, m.SessionID)
|
||||
path := filepath.Join(paths.ArtifactsDir, "trimmed.from-manifest.json")
|
||||
checksum, err := artifacts.SHA256File(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stage.StageResult{Outputs: []artifacts.Ref{{
|
||||
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, SourceID: artifactmodel.SourceTranscriptFinalTrimmed,
|
||||
AbsolutePath: path, Checksum: checksum,
|
||||
}}}, nil
|
||||
}
|
||||
|
||||
func configureLifecycleReferences(t *testing.T, cfg *config.Config) map[string]string {
|
||||
@@ -738,9 +755,13 @@ func extractionLifecycleFixture(t *testing.T, enabled bool) (*config.Config, *st
|
||||
}
|
||||
m := manifest.New(cfg.Session.SessionID, time.Now().UTC())
|
||||
m.Campaign = cfg.Session.Campaign
|
||||
inputChecksum, err := artifacts.SHA256File(inputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SHA256File(input) error = %v", err)
|
||||
}
|
||||
m.MarkStageSucceeded("trim", time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, SourceID: artifactmodel.SourceTranscriptFinalTrimmed,
|
||||
LocalPath: inputPath,
|
||||
LocalPath: inputPath, ProducerRunID: "trim-run", Checksum: inputChecksum,
|
||||
}})
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), paths.ManifestPath, m); err != nil {
|
||||
t.Fatalf("Save(seed) error = %v", err)
|
||||
|
||||
@@ -1276,13 +1276,32 @@ func TestExecutePublishLoadsRemoteLocks(t *testing.T) {
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} {
|
||||
// The publish stage only checks the manifest statuses and source files.
|
||||
_ = stageName
|
||||
}
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.md"), "# final\n")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.md"), "# final trimmed\n")
|
||||
m := manifest.New("2026-05-03", time.Now().UTC())
|
||||
m.Campaign = "sample-campaign"
|
||||
for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "extract", "analyze"} {
|
||||
m.MarkStageSucceeded(stageName, time.Now().UTC(), nil)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(workRoot, "transcripts", "final.trimmed.json"))
|
||||
finalMarkdown := filepath.Join(workRoot, "transcripts", "final.md")
|
||||
trimmedMarkdown := filepath.Join(workRoot, "transcripts", "final.trimmed.md")
|
||||
finalChecksum, err := artifacts.SHA256File(finalMarkdown)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trimmedChecksum, err := artifacts.SHA256File(trimmedMarkdown)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.MarkStageSucceeded("render", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: artifacts.TranscriptOutputKindFinalMarkdown, SourceID: artifacts.ArtifactTranscriptFinalMarkdown, LocalPath: finalMarkdown, ProducerRunID: "render-run", Checksum: finalChecksum},
|
||||
{Kind: artifacts.TranscriptOutputKindFinalTrimmedMarkdown, SourceID: artifacts.ArtifactTranscriptFinalTrimmedMarkdown, LocalPath: trimmedMarkdown, ProducerRunID: "render-run", Checksum: trimmedChecksum},
|
||||
})
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(workRoot, "manifest.json"), m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -1357,10 +1376,14 @@ func writeOperatorExtractionManifest(t *testing.T, workspaceRoot string) string
|
||||
}
|
||||
m := manifest.New("2026-05-03", time.Now().UTC())
|
||||
m.Campaign = "sample-campaign"
|
||||
trimmedChecksum, err := artifacts.SHA256File(trimmedPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Stages["trim"] = &manifest.StageRecord{
|
||||
Name: "trim", Status: manifest.StatusSucceeded,
|
||||
Outputs: []manifest.ArtifactRecord{{
|
||||
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, LocalPath: trimmedPath, ProducerRunID: "trim-run-1",
|
||||
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, SourceID: artifactmodel.SourceTranscriptFinalTrimmed, LocalPath: trimmedPath, ProducerRunID: "trim-run-1", Checksum: trimmedChecksum,
|
||||
}},
|
||||
}
|
||||
input, err := artifacts.ResolveExtractionInputIdentity(paths, m)
|
||||
|
||||
@@ -542,6 +542,7 @@ func publishStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri
|
||||
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} {
|
||||
seedManifest.MarkStageSucceeded(name, time.Now().UTC(), nil)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, seedManifest, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID), "transcripts", "final.trimmed.json"))
|
||||
setAppAnalyzeEvidence(seedManifest, "session_recap", "artifacts/session_recap.md", []byte("# recap\n"))
|
||||
seedManifest.S3SessionPrefix = artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
seedManifest.S3RunPrefix = artifacts.S3RunPrefix(seedManifest.S3SessionPrefix, runID)
|
||||
|
||||
@@ -206,6 +206,14 @@ previous_session_id: 2026-04-26
|
||||
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`+"\n")
|
||||
localManifest, err := (&manifest.LocalStore{}).Load(context.Background(), filepath.Join(sessionRoot, "manifest.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("load restored manifest: %v", err)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, localManifest, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"))
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(sessionRoot, "manifest.json"), localManifest); err != nil {
|
||||
t.Fatalf("save manifest evidence: %v", err)
|
||||
}
|
||||
previousManifestBytes, err := os.ReadFile(filepath.Join(sessionRoot, "previous", "manifest.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read restored previous manifest: %v", err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
@@ -121,7 +122,9 @@ func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
basePath := filepath.Join(workRoot, "transcripts", "base.json")
|
||||
mustWriteTestFile(t, basePath, `{"segments":[]}`)
|
||||
seedCurrentBuiltInEvidence(t, manifestPath, "2026-05-03", "sample-campaign", "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||
|
||||
var out bytes.Buffer
|
||||
@@ -172,11 +175,13 @@ func TestRunStageSkipAndForce(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
basePath := filepath.Join(workRoot, "transcripts", "base.json")
|
||||
mustWriteTestFile(t, basePath, `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||
|
||||
store := &manifest.LocalStore{}
|
||||
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath)
|
||||
m.MarkStageSucceeded("polish", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
|
||||
seedCurrentSemanticEvidence(t, loadConfigForSemanticEvidence(t, pipelinePath, campaignPath, sessionPath), m, "polish")
|
||||
if err := store.Save(context.Background(), manifestPath, m); err != nil {
|
||||
@@ -207,7 +212,9 @@ func TestRunStageForceMarksDownstreamStaleAndRunContinuesFromStale(t *testing.T)
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
basePath := filepath.Join(workRoot, "transcripts", "base.json")
|
||||
mustWriteTestFile(t, basePath, `{"segments":[]}`)
|
||||
seedCurrentBuiltInEvidence(t, manifestPath, "2026-05-03", "sample-campaign", "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath)
|
||||
|
||||
store := &manifest.LocalStore{}
|
||||
materializePrepareResumeFixture(t, pipelinePath, campaignPath, sessionPath)
|
||||
@@ -218,6 +225,7 @@ func TestRunStageForceMarksDownstreamStaleAndRunContinuesFromStale(t *testing.T)
|
||||
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze", "publish", "notify"} {
|
||||
seed.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, seed, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath)
|
||||
seedCurrentSemanticEvidence(t, loadConfigForSemanticEvidence(t, pipelinePath, campaignPath, sessionPath), seed, "prepare", "transcribe", "merge")
|
||||
if err := store.Save(context.Background(), manifestPath, seed); err != nil {
|
||||
t.Fatalf("save manifest: %v", err)
|
||||
@@ -257,7 +265,9 @@ func TestRunStageTrimExecutes(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
finalPath := filepath.Join(workRoot, "transcripts", "final.json")
|
||||
mustWriteTestFile(t, finalPath, `{"segments":[{"id":1},{"id":2}]}`)
|
||||
seedCurrentBuiltInEvidence(t, manifestPath, "2026-05-03", "sample-campaign", "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, finalPath)
|
||||
|
||||
var out bytes.Buffer
|
||||
err := RunStage(context.Background(), []string{"trim", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out)
|
||||
@@ -286,7 +296,9 @@ func TestRunStageNormalizeExecutes(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
polishedPath := filepath.Join(workRoot, "transcripts", "polished.json")
|
||||
mustWriteTestFile(t, polishedPath, `{"segments":[{"id":1},{"id":2}]}`)
|
||||
seedCurrentBuiltInEvidence(t, manifestPath, "2026-05-03", "sample-campaign", "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, polishedPath)
|
||||
|
||||
var out bytes.Buffer
|
||||
err := RunStage(context.Background(), []string{"normalize", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out)
|
||||
@@ -312,8 +324,17 @@ func TestRunStageRenderExecutes(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.json"), `{"segments":[{"id":1}]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[{"id":2}]}`)
|
||||
finalPath := filepath.Join(workRoot, "transcripts", "final.json")
|
||||
trimmedPath := filepath.Join(workRoot, "transcripts", "final.trimmed.json")
|
||||
mustWriteTestFile(t, finalPath, `{"segments":[{"id":1}]}`)
|
||||
mustWriteTestFile(t, trimmedPath, `{"segments":[{"id":2}]}`)
|
||||
seed := manifest.New("2026-05-03", time.Now().UTC())
|
||||
seed.Campaign = "sample-campaign"
|
||||
setCurrentBuiltInEvidence(t, seed, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, finalPath)
|
||||
setCurrentBuiltInEvidence(t, seed, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, trimmedPath)
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), manifestPath, seed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err := RunStage(context.Background(), []string{"render", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||
)
|
||||
|
||||
// MaxResolvedArtifactBytes bounds externally generated artifact content before validation.
|
||||
@@ -178,8 +180,8 @@ func PreviousSessionArtifactName(source string) (string, bool) {
|
||||
return artifactpolicy.ParsePreviousSessionSource(source)
|
||||
}
|
||||
|
||||
// ResolveSessionArtifact resolves a symbolic source to a readable local session artifact path.
|
||||
// Resolution order is manifest producer outputs first, then canonical session path fallback.
|
||||
// ResolveSessionArtifact resolves a symbolic source from its unique current
|
||||
// producer output declaration in the session manifest.
|
||||
func ResolveSessionArtifact(paths SessionPaths, m *manifest.Manifest, source string) (ResolvedSessionArtifact, error) {
|
||||
id, err := NormalizeSessionArtifactSource(source)
|
||||
if err != nil {
|
||||
@@ -187,43 +189,21 @@ func ResolveSessionArtifact(paths SessionPaths, m *manifest.Manifest, source str
|
||||
}
|
||||
spec := artifactRegistry[id]
|
||||
|
||||
for _, candidate := range manifestArtifactCandidates(paths, m, spec) {
|
||||
exists, isDir, statErr := pathExists(candidate.Path)
|
||||
if statErr != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("stat %q: %w", candidate.Path, statErr)
|
||||
}
|
||||
if !exists || isDir {
|
||||
continue
|
||||
}
|
||||
resolved := candidate
|
||||
resolved.ID = spec.ID
|
||||
resolved.ProducerStage = spec.ProducerStage
|
||||
resolved.OutputKind = spec.OutputKind
|
||||
if err := validateResolvedContent(resolved.Path, spec.ContentKind); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", resolved.ID, err)
|
||||
}
|
||||
return resolved, nil
|
||||
candidate, err := manifestArtifactCandidate(paths, m, spec)
|
||||
if err != nil {
|
||||
return ResolvedSessionArtifact{}, err
|
||||
}
|
||||
|
||||
fallbackPath := filepath.Join(paths.Root, filepath.FromSlash(spec.CanonicalRelPath))
|
||||
exists, isDir, statErr := pathExists(fallbackPath)
|
||||
if statErr != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("stat %q: %w", fallbackPath, statErr)
|
||||
if err := validateResolvedContent(candidate.Path, spec.ContentKind); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", spec.ID, err)
|
||||
}
|
||||
if exists && !isDir {
|
||||
if err := validateResolvedContent(fallbackPath, spec.ContentKind); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", spec.ID, err)
|
||||
}
|
||||
return ResolvedSessionArtifact{
|
||||
ID: spec.ID,
|
||||
Path: filepath.Clean(fallbackPath),
|
||||
ProducerStage: spec.ProducerStage,
|
||||
OutputKind: spec.OutputKind,
|
||||
Provenance: "fallback.canonical_path",
|
||||
}, nil
|
||||
actualChecksum, err := SHA256File(candidate.Path)
|
||||
if err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("checksum %q: %w", spec.ID, err)
|
||||
}
|
||||
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: spec.ID}
|
||||
if actualChecksum != candidate.Checksum {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: checksum mismatch", spec.ID)
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
// ResolveSessionArtifactWithCatalog resolves built-in sources using existing rules and resolves
|
||||
@@ -334,52 +314,74 @@ func ResolvePreviousSessionArtifactWithCatalog(
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
|
||||
}
|
||||
|
||||
func manifestArtifactCandidates(paths SessionPaths, m *manifest.Manifest, spec artifactSpec) []ResolvedSessionArtifact {
|
||||
if m == nil || len(m.Stages) == 0 || spec.ProducerStage == "" || spec.OutputKind == "" {
|
||||
return nil
|
||||
func manifestArtifactCandidate(paths SessionPaths, m *manifest.Manifest, spec artifactSpec) (ResolvedSessionArtifact, error) {
|
||||
if m == nil || spec.ProducerStage == "" || spec.OutputKind == "" {
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: spec.ID}
|
||||
}
|
||||
sr := m.Stages[spec.ProducerStage]
|
||||
if sr == nil {
|
||||
return nil
|
||||
if sr == nil || sr.Name != spec.ProducerStage || sr.Status != manifest.StatusSucceeded {
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: spec.ID}
|
||||
}
|
||||
|
||||
candidates := make([]ResolvedSessionArtifact, 0, len(sr.Outputs))
|
||||
var selected manifest.ArtifactRecord
|
||||
count := 0
|
||||
for _, out := range sr.Outputs {
|
||||
if strings.TrimSpace(out.Kind) != spec.OutputKind {
|
||||
continue
|
||||
if strings.TrimSpace(out.Kind) == spec.OutputKind && strings.TrimSpace(out.SourceID) == spec.ID {
|
||||
selected, count = out, count+1
|
||||
}
|
||||
if strings.TrimSpace(out.LocalPath) == "" {
|
||||
continue
|
||||
}
|
||||
resolved := filepath.Clean(ResolveSessionLocalPathForRead(paths, out.LocalPath))
|
||||
if resolved == "" {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, ResolvedSessionArtifact{
|
||||
Path: resolved,
|
||||
ProducerRunID: strings.TrimSpace(out.ProducerRunID),
|
||||
Provenance: "manifest." + spec.ProducerStage + ".outputs",
|
||||
})
|
||||
}
|
||||
return dedupeResolvedArtifacts(candidates)
|
||||
if count == 0 {
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: spec.ID}
|
||||
}
|
||||
if count != 1 {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q is ambiguous", spec.ProducerStage, spec.ID)
|
||||
}
|
||||
if err := ValidateRunIdentity(strings.TrimSpace(selected.ProducerRunID)); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has invalid producer run ID: %w", spec.ProducerStage, spec.ID, err)
|
||||
}
|
||||
checksum := strings.TrimSpace(selected.Checksum)
|
||||
if len(checksum) != 64 || strings.ToLower(checksum) != checksum {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has invalid checksum", spec.ProducerStage, spec.ID)
|
||||
}
|
||||
if _, err := hex.DecodeString(checksum); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has invalid checksum", spec.ProducerStage, spec.ID)
|
||||
}
|
||||
resolved := filepath.Clean(ResolveSessionLocalPathForRead(paths, selected.LocalPath))
|
||||
if resolved == "" {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has an empty path", spec.ProducerStage, spec.ID)
|
||||
}
|
||||
if _, err := pathsafe.SlashRelativeFromRoot(paths.Root, resolved); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has unsafe path: %w", spec.ProducerStage, spec.ID, err)
|
||||
}
|
||||
if err := requireSafeSessionRegularFile(paths.Root, resolved); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("manifest %s output evidence for %q has unsafe path: %w", spec.ProducerStage, spec.ID, err)
|
||||
}
|
||||
return ResolvedSessionArtifact{ID: spec.ID, Path: resolved, ProducerStage: spec.ProducerStage, OutputKind: spec.OutputKind, ProducerRunID: selected.ProducerRunID, Provenance: "manifest." + spec.ProducerStage + ".outputs", Checksum: checksum}, nil
|
||||
}
|
||||
|
||||
func dedupeResolvedArtifacts(values []ResolvedSessionArtifact) []ResolvedSessionArtifact {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]ResolvedSessionArtifact, 0, len(values))
|
||||
for _, value := range values {
|
||||
key := filepath.Clean(strings.TrimSpace(value.Path))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
value.Path = key
|
||||
out = append(out, value)
|
||||
func requireSafeSessionRegularFile(root, path string) error {
|
||||
relative, err := pathsafe.SlashRelativeFromRoot(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return out
|
||||
current := filepath.Clean(root)
|
||||
for _, component := range strings.Split(relative, "/") {
|
||||
current = filepath.Join(current, filepath.FromSlash(component))
|
||||
info, err := os.Lstat(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("path contains symlink %q", current)
|
||||
}
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("path is not a regular file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func previousSessionCacheCandidatePaths(paths SessionPaths, canonicalRelPath string) []string {
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
|
||||
|
||||
m := manifest.New("session", time.Now().UTC())
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_final", LocalPath: manifestPath, ProducerRunID: "run-123"},
|
||||
{Kind: "transcript_final", SourceID: ArtifactTranscriptFinal, LocalPath: manifestPath, ProducerRunID: "run-123", Checksum: resolverTestChecksum(t, manifestPath)},
|
||||
})
|
||||
|
||||
resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFinal)
|
||||
@@ -205,7 +205,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
|
||||
func TestResolveSessionArtifactRejectsIncidentalCanonicalPath(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
@@ -216,19 +216,13 @@ func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
|
||||
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)
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
|
||||
if !errors.Is(err, ErrSessionArtifactNotFound) {
|
||||
t.Fatalf("ResolveSessionArtifact() error = %v, want typed unavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSessionArtifactFallsBackToCanonicalMarkdownPath(t *testing.T) {
|
||||
func TestResolveSessionArtifactRejectsIncidentalCanonicalMarkdownPath(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.md")
|
||||
@@ -239,15 +233,9 @@ func TestResolveSessionArtifactFallsBackToCanonicalMarkdownPath(t *testing.T) {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalMarkdown)
|
||||
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)
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalMarkdown)
|
||||
if !errors.Is(err, ErrSessionArtifactNotFound) {
|
||||
t.Fatalf("ResolveSessionArtifact() error = %v, want typed unavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +263,8 @@ func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptPolished)
|
||||
m := resolverTestManifest(t, paths, ArtifactTranscriptPolished, canonicalPath)
|
||||
_, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptPolished)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
@@ -295,7 +284,8 @@ func TestResolveSessionArtifactRejectsEmptyMarkdownContent(t *testing.T) {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmedMarkdown)
|
||||
m := resolverTestManifest(t, paths, ArtifactTranscriptFinalTrimmedMarkdown, canonicalPath)
|
||||
_, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFinalTrimmedMarkdown)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
@@ -304,7 +294,7 @@ func TestResolveSessionArtifactRejectsEmptyMarkdownContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) {
|
||||
func TestResolveSessionArtifactWithCatalogBuiltInUsesManifestEvidence(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
@@ -315,18 +305,39 @@ func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T)
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptFinalTrimmed, NewArtifactCatalog())
|
||||
m := resolverTestManifest(t, paths, ArtifactTranscriptFinalTrimmed, canonicalPath)
|
||||
resolved, err := ResolveSessionArtifactWithCatalog(paths, m, ArtifactTranscriptFinalTrimmed, 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)
|
||||
if resolved.Provenance != "manifest.trim.outputs" {
|
||||
t.Fatalf("provenance = %q, want manifest.trim.outputs", resolved.Provenance)
|
||||
}
|
||||
}
|
||||
|
||||
func resolverTestManifest(t *testing.T, paths SessionPaths, source, localPath string) *manifest.Manifest {
|
||||
t.Helper()
|
||||
spec := artifactRegistry[source]
|
||||
m := manifest.New(paths.SessionID, time.Now().UTC())
|
||||
m.MarkStageSucceeded(spec.ProducerStage, time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: spec.OutputKind, SourceID: source, LocalPath: localPath,
|
||||
ProducerRunID: "resolver-run", Checksum: resolverTestChecksum(t, localPath),
|
||||
}})
|
||||
return m
|
||||
}
|
||||
|
||||
func resolverTestChecksum(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
checksum, err := SHA256File(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
func TestResolveSessionArtifactWithCatalogConfiguredAvailableGenerated(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -93,6 +94,7 @@ func TestInspectExtractionEvidenceClassifiesBundleStates(t *testing.T) {
|
||||
name string
|
||||
state ExtractionEvidenceState
|
||||
recomputeInputIdentity bool
|
||||
wantInputUnavailable bool
|
||||
mutate func(*testing.T, *SessionPaths, *manifest.Manifest)
|
||||
}{
|
||||
{name: "valid", state: ExtractionEvidenceValid, mutate: func(_ *testing.T, _ *SessionPaths, _ *manifest.Manifest) {}},
|
||||
@@ -102,7 +104,7 @@ func TestInspectExtractionEvidenceClassifiesBundleStates(t *testing.T) {
|
||||
{name: "obsolete version", state: ExtractionEvidenceObsolete, mutate: func(_ *testing.T, _ *SessionPaths, m *manifest.Manifest) {
|
||||
m.Stages["extract"].Outputs[0].Contract.SchemaVersion = "99"
|
||||
}},
|
||||
{name: "changed direct transcript", state: ExtractionEvidenceObsolete, recomputeInputIdentity: true, mutate: func(t *testing.T, _ *SessionPaths, m *manifest.Manifest) {
|
||||
{name: "changed direct transcript", wantInputUnavailable: true, recomputeInputIdentity: true, mutate: func(t *testing.T, _ *SessionPaths, m *manifest.Manifest) {
|
||||
writeExtractionFixtureFile(t, m.Stages["trim"].Outputs[0].LocalPath, `{"segments":[{"id":"changed"}]}`)
|
||||
}},
|
||||
{name: "changed direct producer", state: ExtractionEvidenceObsolete, recomputeInputIdentity: true, mutate: func(_ *testing.T, _ *SessionPaths, m *manifest.Manifest) {
|
||||
@@ -140,6 +142,12 @@ func TestInspectExtractionEvidenceClassifiesBundleStates(t *testing.T) {
|
||||
test.mutate(t, &paths, currentManifest)
|
||||
if test.recomputeInputIdentity {
|
||||
input, err = ResolveExtractionInputIdentity(paths, currentManifest)
|
||||
if test.wantInputUnavailable {
|
||||
if !errors.Is(err, ErrSessionArtifactNotFound) && (err == nil || !strings.Contains(err.Error(), "checksum mismatch")) {
|
||||
t.Fatalf("ResolveExtractionInputIdentity() error = %v, want unavailable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -271,7 +279,7 @@ func validExtractionCatalogFixture(t *testing.T) (SessionPaths, *manifest.Manife
|
||||
m.Stages["trim"] = &manifest.StageRecord{
|
||||
Name: "trim", Status: manifest.StatusSucceeded,
|
||||
Outputs: []manifest.ArtifactRecord{{
|
||||
Kind: TranscriptOutputKindFinalTrimmed, LocalPath: trimmedPath, ProducerRunID: "trim-run-1",
|
||||
Kind: TranscriptOutputKindFinalTrimmed, SourceID: ArtifactTranscriptFinalTrimmed, LocalPath: trimmedPath, ProducerRunID: "trim-run-1", Checksum: extractionFixtureChecksum(t, trimmedPath),
|
||||
}},
|
||||
}
|
||||
input, err := ResolveExtractionInputIdentity(paths, m)
|
||||
|
||||
@@ -43,3 +43,15 @@ func setCurrentAnalyzeEvidence(t *testing.T, m *manifest.Manifest, key, relative
|
||||
OutputSize: info.Size(), ProducerRunID: "run-1", UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentBuiltInEvidence(t *testing.T, m *manifest.Manifest, stage, kind, sourceID, absolutePath string) {
|
||||
t.Helper()
|
||||
checksum, err := artifacts.SHA256File(absolutePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.MarkStageSucceeded(stage, time.Date(2026, 5, 16, 1, 2, 3, 0, time.UTC), []manifest.ArtifactRecord{{
|
||||
Kind: kind, SourceID: sourceID, LocalPath: absolutePath,
|
||||
ProducerRunID: stage + "-run", Checksum: checksum,
|
||||
}})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestResolveAnalyzeInputIdentitiesSupportsEverySourceKind(t *testing.T) {
|
||||
setup: func(t *testing.T, env *Env, m *manifest.Manifest) string {
|
||||
path := filepath.Join(sessionPathsForEnv(env, m.SessionID).TranscriptsDir, "final.trimmed.json")
|
||||
writeAnalyzeFile(t, path, `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, path)
|
||||
return path
|
||||
},
|
||||
wantOutputKind: "transcript_final_trimmed",
|
||||
@@ -47,6 +48,7 @@ func TestResolveAnalyzeInputIdentitiesSupportsEverySourceKind(t *testing.T) {
|
||||
setup: func(t *testing.T, env *Env, m *manifest.Manifest) string {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"))
|
||||
return configureAnalyzeExtractionFixture(t, env, m)["encounters"]
|
||||
},
|
||||
wantOutputKind: "notarius_lane",
|
||||
@@ -177,6 +179,9 @@ func TestResolveAnalyzeInputIdentitiesRejectsUnsafePathsAndFileTypes(t *testing.
|
||||
setup: func(t *testing.T, env *Env, m *manifest.Manifest) {
|
||||
t.Helper()
|
||||
path := filepath.Join(sessionPathsForEnv(env, m.SessionID).TranscriptsDir, "final.trimmed.json")
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -213,6 +218,7 @@ func TestAnalyzeInputIdentityIsStableAcrossWorkspaceRelocationAndChangesWithByte
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
path := filepath.Join(sessionPathsForEnv(env, m.SessionID).TranscriptsDir, "final.trimmed.json")
|
||||
writeAnalyzeFile(t, path, body)
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, path)
|
||||
resolved, err := resolveAnalyzeInputIdentities(map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {Source: "narratio.transcript.final_trimmed", Required: true},
|
||||
}, newAnalyzeIdentityExecution(t, env, m))
|
||||
|
||||
@@ -151,7 +151,6 @@ func TestPlanAnalyzeWorkSchedulesEveryNonCurrentReason(t *testing.T) {
|
||||
analyzeReconciliationStale,
|
||||
analyzeReconciliationMissing,
|
||||
analyzeReconciliationFailed,
|
||||
analyzeReconciliationLegacy,
|
||||
analyzeReconciliationNonResumable,
|
||||
}
|
||||
for _, reason := range reasons {
|
||||
@@ -245,12 +244,12 @@ func TestPlanAnalyzeWorkProjectionPreservesOnlyValidConfiguredAuthority(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanAnalyzeWorkLegacyPartialSelectionDoesNotPromoteUnselectedOutput(t *testing.T) {
|
||||
func TestPlanAnalyzeWorkNonResumablePartialSelectionDoesNotPromoteUnselectedOutput(t *testing.T) {
|
||||
configured := map[string]config.ScriptoriumArtifactConfig{
|
||||
"target": {Enabled: true}, "legacy_unselected": {Enabled: true},
|
||||
}
|
||||
reconciliation := analyzePlanningReconciliation(configured, map[string]analyzeReconciliationReason{
|
||||
"target": analyzeReconciliationLegacy, "legacy_unselected": analyzeReconciliationLegacy,
|
||||
"target": analyzeReconciliationNonResumable, "legacy_unselected": analyzeReconciliationNonResumable,
|
||||
}, nil)
|
||||
plan, err := planAnalyzeWork(&config.ScriptoriumConfig{Artifacts: configured}, []string{"target"}, false, reconciliation)
|
||||
if err != nil {
|
||||
@@ -258,7 +257,7 @@ func TestPlanAnalyzeWorkLegacyPartialSelectionDoesNotPromoteUnselectedOutput(t *
|
||||
}
|
||||
assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, []string{"target"})
|
||||
if len(plan.ProjectedRecords) != 0 {
|
||||
t.Fatalf("legacy projection = %#v, want no current records", plan.ProjectedRecords)
|
||||
t.Fatalf("non-resumable projection = %#v, want no current records", plan.ProjectedRecords)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ const (
|
||||
analyzeReconciliationStale analyzeReconciliationReason = "stale"
|
||||
analyzeReconciliationMissing analyzeReconciliationReason = "missing"
|
||||
analyzeReconciliationFailed analyzeReconciliationReason = "failed"
|
||||
analyzeReconciliationLegacy analyzeReconciliationReason = "legacy"
|
||||
analyzeReconciliationRemoved analyzeReconciliationReason = "removed"
|
||||
analyzeReconciliationNonResumable analyzeReconciliationReason = "non_resumable"
|
||||
)
|
||||
@@ -109,8 +108,8 @@ func reconcileAnalyzeArtifact(
|
||||
return result
|
||||
}
|
||||
if stageRecord.AnalyzeStateVersion == 0 {
|
||||
result.Reason = analyzeReconciliationLegacy
|
||||
result.Detail = "analyze manifest uses aggregate-only legacy state"
|
||||
result.Reason = analyzeReconciliationNonResumable
|
||||
result.Detail = "analyze manifest state contract is absent"
|
||||
return result
|
||||
}
|
||||
if stageRecord.AnalyzeStateVersion != manifest.AnalyzeStateContractVersion {
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestReconcileAnalyzeArtifactsClassifiesStoredState(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy record", want: analyzeReconciliationLegacy,
|
||||
name: "missing state contract", want: analyzeReconciliationNonResumable,
|
||||
mutate: func(_ *testing.T, _ *Env, m *manifest.Manifest) {
|
||||
m.Stages["analyze"].AnalyzeStateVersion = 0
|
||||
m.Stages["analyze"].AnalyzeArtifacts = nil
|
||||
|
||||
@@ -264,6 +264,7 @@ func TestAnalyzeOmitsOptionalCanonicalPreviousRecapWhenUnavailable(t *testing.T)
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -303,6 +304,7 @@ func TestAnalyzeUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"))
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -386,6 +388,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
previousRecapPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousRecapPath, "previous recap\n")
|
||||
@@ -938,6 +941,7 @@ func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) {
|
||||
|
||||
func TestAnalyzeFailsWhenTrimmedTranscriptMissing(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
delete(m.Stages, "trim")
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -964,6 +968,9 @@ func TestAnalyzeMissingOptionalBuiltInInputsAreOmitted(t *testing.T) {
|
||||
"optional": {Source: source, Required: false},
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
if source == artifacts.ArtifactTranscriptFinalTrimmed {
|
||||
delete(m.Stages, "trim")
|
||||
}
|
||||
|
||||
if _, err := (analyzeStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
@@ -1000,6 +1007,9 @@ func TestAnalyzeMissingRequiredBuiltInInputsProvideExecutableGuidance(t *testing
|
||||
"required": {Source: tt.source, Required: true},
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
if tt.source == artifacts.ArtifactTranscriptFinalTrimmed {
|
||||
delete(m.Stages, "trim")
|
||||
}
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -1020,6 +1030,7 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -1069,6 +1080,7 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalizedPath := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeAnalyzeFile(t, normalizedPath, `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, normalizedPath)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -1094,6 +1106,7 @@ func TestAnalyzeSupportsRenderedMarkdownTranscriptSourceWhenConfigured(t *testin
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
markdownPath := filepath.Join(paths.TranscriptsDir, "final.md")
|
||||
writeAnalyzeFile(t, markdownPath, "# Session Transcript\n")
|
||||
setCurrentBuiltInEvidence(t, m, "render", artifacts.TranscriptOutputKindFinalMarkdown, artifacts.ArtifactTranscriptFinalMarkdown, markdownPath)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -1243,9 +1256,7 @@ func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t
|
||||
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
|
||||
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
|
||||
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_final", LocalPath: manifestPath},
|
||||
})
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, manifestPath)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -1273,9 +1284,7 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.
|
||||
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
|
||||
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
|
||||
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_final", LocalPath: manifestPath},
|
||||
})
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, manifestPath)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -1689,6 +1698,17 @@ func setupAnalyzeEnv(t *testing.T) (*Env, *manifest.Manifest, *scriptorium.FakeR
|
||||
Scriptorium: fake,
|
||||
}
|
||||
m := manifest.New("2026-05-03", time.Now().UTC())
|
||||
paths := store.SessionPathsFor("icewind-dale", "2026-05-03")
|
||||
trimmedPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
writeAnalyzeFile(t, trimmedPath, `{"segments":[]}`)
|
||||
checksum, err := artifacts.SHA256File(trimmedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SHA256File(%q): %v", trimmedPath, err)
|
||||
}
|
||||
m.MarkStageSucceeded("trim", time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: artifacts.TranscriptOutputKindFinalTrimmed, SourceID: artifacts.ArtifactTranscriptFinalTrimmed,
|
||||
LocalPath: trimmedPath, ProducerRunID: "trim-run", Checksum: checksum,
|
||||
}})
|
||||
return env, m, fake
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func (extractStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
|
||||
paths := sessionPathsForEnv(env, sessionID)
|
||||
input, err := artifacts.ResolveExtractionInputIdentity(paths, m)
|
||||
if err != nil {
|
||||
return ResumeValidation{}, fmt.Errorf("extract resume: resolve final-trimmed transcript identity: %w", err)
|
||||
return NonResumable("final-trimmed transcript evidence is unavailable or changed"), nil
|
||||
}
|
||||
references, err := resolveExtractReferences(paths, m, cfg, sessionID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1012,9 +1012,13 @@ func setupExtractEnv(t *testing.T) (*Env, *manifest.Manifest, *notarius.FakeRunn
|
||||
m := manifest.New(sessionID, time.Now().UTC())
|
||||
m.Campaign = campaign
|
||||
m.RunID = runID
|
||||
inputChecksum, err := artifacts.SHA256File(inputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SHA256File(input) error = %v", err)
|
||||
}
|
||||
m.MarkStageSucceeded("trim", time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, SourceID: artifactmodel.SourceTranscriptFinalTrimmed,
|
||||
LocalPath: inputPath,
|
||||
LocalPath: inputPath, ProducerRunID: "trim-run", Checksum: inputChecksum,
|
||||
}})
|
||||
|
||||
outputRoot := artifacts.SessionRunNotariusOutputRootForCampaign(workspace, campaign, sessionID, runID)
|
||||
|
||||
@@ -20,10 +20,7 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
|
||||
manifestProcessed := filepath.Join(paths.ArtifactsDir, "processed.from-manifest.json")
|
||||
writeFile(t, manifestProcessed, `{"segments":[{"id":10}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":99}]}`)
|
||||
m.MarkStageSucceeded("polish", time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: "transcript_polished",
|
||||
LocalPath: manifestProcessed,
|
||||
}})
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, manifestProcessed)
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -41,21 +38,18 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeStageFallsBackToProcessedTranscriptPath(t *testing.T) {
|
||||
func TestNormalizeStageRejectsIncidentalProcessedTranscriptPath(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "polished.json")
|
||||
writeFile(t, fallback, `{"segments":[{"id":1}]}`)
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize.Run() error = %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "processed transcript input is required") {
|
||||
t.Fatalf("normalize.Run() error = %v, want unavailable manifest input", err)
|
||||
}
|
||||
if len(ser.NormalizeRequests) != 1 {
|
||||
t.Fatalf("normalize requests = %d, want 1", len(ser.NormalizeRequests))
|
||||
}
|
||||
if ser.NormalizeRequests[0].InputTranscriptPath != fallback {
|
||||
t.Fatalf("input transcript = %q, want %q", ser.NormalizeRequests[0].InputTranscriptPath, fallback)
|
||||
if len(ser.NormalizeRequests) != 0 {
|
||||
t.Fatalf("normalize requests = %d, want 0", len(ser.NormalizeRequests))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +68,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), "not-json")
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -88,6 +83,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T)
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"schema":"audita.processed.v1"}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -102,6 +98,7 @@ func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
env.Config.Pipeline.Normalize.OutputSchema = "seriatim-full"
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
@@ -123,6 +120,7 @@ func TestNormalizeStageRecordsNormalizedTranscriptOutputKind(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -140,6 +138,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
report := true
|
||||
env.Config.Pipeline.Normalize.Report = &report
|
||||
|
||||
@@ -166,6 +165,7 @@ func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
ser.NormalizeErr = errors.New("normalize failed")
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
@@ -181,6 +181,7 @@ func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
badOutput := filepath.Join(paths.TranscriptsDir, "normalized.bad.json")
|
||||
writeFile(t, badOutput, "not-json")
|
||||
ser.NormalizeResult = seriatim.NormalizeResult{OutputNormalizedPath: badOutput}
|
||||
@@ -198,6 +199,7 @@ func TestNormalizeStageReportEnabledFailsWhenReportMissing(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
report := true
|
||||
env.Config.Pipeline.Normalize.Report = &report
|
||||
ser.NormalizeResult = seriatim.NormalizeResult{ReportPath: filepath.Join(paths.ArtifactsDir, "missing-report.json")}
|
||||
@@ -218,6 +220,7 @@ func TestNormalizeStageUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "polish", artifacts.TranscriptOutputKindPolished, artifacts.ArtifactTranscriptPolished, filepath.Join(paths.TranscriptsDir, "polished.json"))
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,9 +21,7 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
writeFile(t, mergedPath, `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
m.MarkStageSucceeded("merge", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_base", LocalPath: mergedPath},
|
||||
})
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, mergedPath)
|
||||
|
||||
fake := &audita.FakeRunner{}
|
||||
env.Audita = fake
|
||||
@@ -84,7 +82,7 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) {
|
||||
func TestPolishStageRejectsIncidentalMergedTranscriptPath(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
@@ -95,14 +93,11 @@ func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) {
|
||||
env.Audita = fake
|
||||
|
||||
_, err := (polishStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("polish.Run() error = %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "merged transcript input is required") {
|
||||
t.Fatalf("polish.Run() error = %v, want unavailable manifest input", err)
|
||||
}
|
||||
if len(fake.Requests) != 1 {
|
||||
t.Fatalf("requests = %d, want 1", len(fake.Requests))
|
||||
}
|
||||
if fake.Requests[0].MergedTranscriptPath != mergedPath {
|
||||
t.Fatalf("merged path = %q, want %q", fake.Requests[0].MergedTranscriptPath, mergedPath)
|
||||
if len(fake.Requests) != 0 {
|
||||
t.Fatalf("requests = %d, want 0", len(fake.Requests))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +120,7 @@ func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), "not-json")
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
env.Audita = &audita.FakeRunner{}
|
||||
|
||||
@@ -141,6 +137,7 @@ func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
env.Audita = &audita.FakeRunner{}
|
||||
|
||||
_, err := (polishStage{}).Run(context.Background(), env, m)
|
||||
@@ -156,6 +153,7 @@ func TestPolishStageFailsWhenAdapterFails(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
env.Audita = &audita.FakeRunner{Err: errors.New("audita failed")}
|
||||
|
||||
@@ -172,6 +170,7 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
badPath := filepath.Join(paths.TranscriptsDir, "processed.invalid.json")
|
||||
writeFile(t, badPath, `{"schema":"audita.processed.v1","segments":"wrong-type"}`)
|
||||
@@ -190,6 +189,7 @@ func TestPolishStageFailsWhenReportInvalid(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json")
|
||||
writeFile(t, badReport, "not-json")
|
||||
@@ -211,6 +211,7 @@ func TestPolishStageUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
|
||||
fake := &audita.FakeRunner{}
|
||||
|
||||
@@ -1071,6 +1071,7 @@ func publishFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
for _, name := range publishPrerequisiteStages {
|
||||
m.MarkStageSucceeded(name, time.Date(2026, 5, 16, 1, 2, 3, 0, time.UTC), nil)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"))
|
||||
setCurrentAnalyzeEvidence(t, m, "session_recap", "artifacts/session_recap.md", filepath.Join(sessionRoot, "artifacts", "session_recap.md"))
|
||||
|
||||
env := &Env{
|
||||
|
||||
@@ -44,8 +44,8 @@ func TestRenderStageResolvesManifestInputsFirst(t *testing.T) {
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[{"id":2}]}`)
|
||||
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{{Kind: "transcript_final", LocalPath: manifestFinal}})
|
||||
m.MarkStageSucceeded("trim", time.Now().UTC(), []manifest.ArtifactRecord{{Kind: "transcript_final_trimmed", LocalPath: manifestTrimmed}})
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, manifestFinal)
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, manifestTrimmed)
|
||||
|
||||
result, err := (renderStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -82,6 +82,8 @@ func TestRenderStageTitleFallsBackToSessionTitle(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"))
|
||||
env.Config.Pipeline.Render.Title = ""
|
||||
env.Config.Session.Title = "Session Title"
|
||||
|
||||
@@ -112,6 +114,7 @@ func TestRenderStageFailsWhenFinalTrimmedInputMissing(t *testing.T) {
|
||||
env, m, _ := setupRenderEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
|
||||
_, err := (renderStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -130,6 +133,8 @@ func TestRenderStageUsesRunLocalOutputsAndMaterializesCanonical(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"))
|
||||
|
||||
result, err := (renderStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,9 +21,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":868}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_final", LocalPath: normalized},
|
||||
})
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, normalized)
|
||||
scr.BoundsBody = `{"confidence":"high","trim_action":"trim","start_segment_id":10,"end_segment_id":868,"warnings":[]}`
|
||||
|
||||
result, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -76,6 +74,7 @@ func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, normalized)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11}`
|
||||
|
||||
cfg := env.Config.Pipeline.Trim
|
||||
@@ -99,6 +98,7 @@ func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
|
||||
result, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -119,6 +119,7 @@ func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
|
||||
cfg := *env.Config.Pipeline.Trim
|
||||
@@ -183,6 +184,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m, _, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), "not-json")
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -196,6 +198,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T
|
||||
env, m, _, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"schema":"audita.processed.v1"}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -209,6 +212,7 @@ func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = "not-json"
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -223,6 +227,7 @@ func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}`
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -237,6 +242,7 @@ func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":20},{"id":21}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}`
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -251,6 +257,7 @@ func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.RunErr = errors.New("bounds failed")
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -265,6 +272,7 @@ func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
|
||||
env, m, scr, ser := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, filepath.Join(paths.TranscriptsDir, "final.json"))
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
ser.TrimErr = errors.New("trim failed")
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -282,6 +290,7 @@ func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
normalizedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}`
|
||||
writeFile(t, normalized, normalizedBody)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, normalized)
|
||||
|
||||
disabled := *env.Config.Pipeline.Trim
|
||||
enabled := false
|
||||
@@ -316,6 +325,7 @@ func TestTrimStageUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
|
||||
setCurrentBuiltInEvidence(t, m, "normalize", artifacts.TranscriptOutputKindFinal, artifacts.ArtifactTranscriptFinal, normalized)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11,"warnings":[]}`
|
||||
|
||||
result, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
|
||||
Reference in New Issue
Block a user