package app import ( "context" "errors" "io" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "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/stage" ) type publishSuccessStage struct { metadata map[string]any targets *cleanupSeed } func (publishSuccessStage) Name() string { return "publish" } func (publishSuccessStage) Declares() stage.IODecl { return stage.IODecl{} } func (s publishSuccessStage) Run(_ context.Context, _ *stage.Env, m *manifest.Manifest) (*stage.StageResult, error) { if s.targets != nil { s.targets.runWorkDir = m.LocalWorkDir s.targets.spoolAudioDir = m.LocalSpoolDir if err := os.MkdirAll(filepath.Join(m.LocalWorkDir, "logs"), 0o755); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(m.LocalWorkDir, "logs", "stage.log"), []byte("log\n"), 0o644); err != nil { return nil, err } if err := os.MkdirAll(m.LocalSpoolDir, 0o755); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(m.LocalSpoolDir, "speaker.flac"), []byte("flac\n"), 0o644); err != nil { return nil, err } } md := map[string]any{ "stage": "publish", "uploaded": true, "published_run_id": m.RunID, "remote_commit_key": "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260519T010203Z-a1b2c3d4/commit.json", "current_commit_pointer_key": "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/commit-pointer.json", } for k, v := range s.metadata { md[k] = v } return &stage.StageResult{Metadata: md}, nil } type notifyFailStage struct{} func (notifyFailStage) Name() string { return "notify" } func (notifyFailStage) Declares() stage.IODecl { return stage.IODecl{} } func (notifyFailStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) { return nil, errors.New("notify failed") } func TestPostPublishCleanupDisabledKeepsLocalDirs(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = false cfg.Pipeline.Workspace.CleanupAfterPublish = false if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) assertExists(t, seed.localSourceAudio) } func TestPostPublishCleanupSpoolOnly(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertMissing(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) assertExists(t, seed.localSourceAudio) } func TestPostPublishCleanupWorkdirOnly(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = false cfg.Pipeline.Workspace.CleanupAfterPublish = true if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertExists(t, cfg.Pipeline.Workspace.Root) assertExists(t, seed.otherRunDir) assertExists(t, seed.previousCachePath) assertMissing(t, seed.runWorkDir) assertExists(t, seed.spoolAudioDir) } func TestPostPublishCleanupBothPolicies(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertMissing(t, seed.spoolAudioDir) assertMissing(t, seed.runWorkDir) assertExists(t, seed.otherRunDir) assertExists(t, seed.previousCachePath) } func TestPostPublishCleanupRetriesWhenInitialObligationSaveFails(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false failed := false store := &cleanupFailingManifestStore{ delegate: &manifest.LocalStore{}, fail: func(m *manifest.Manifest) error { if !failed && m.PostPublishCleanup != nil { failed = true return errors.New("injected obligation save failure") } return nil }, } _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{ Env: &Env{ManifestStore: store, ObjectStore: &storage.FakeBackend{}}, }) if err == nil || !strings.Contains(err.Error(), "post-publish cleanup incomplete") { t.Fatalf("executeStages() error = %v, want incomplete cleanup", err) } assertExists(t, seed.spoolAudioDir) assertCleanupPending(t, cfg) store.fail = nil if _, err := executeStages(context.Background(), cfg, nil, RunOptions{Env: &Env{ManifestStore: store, ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("retry executeStages() error = %v", err) } assertMissing(t, seed.spoolAudioDir) assertCleanupComplete(t, cfg) } func TestPostPublishCleanupRetriesFailedDeletionWithoutTouchingOtherRuns(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true originalRemove := removeRunScopedDirFn removeRunScopedDirFn = func(root, target, policy string) error { if policy == "pipeline.workspace.cleanup_after_publish" { return errors.New("injected deletion failure") } return originalRemove(root, target, policy) } t.Cleanup(func() { removeRunScopedDirFn = originalRemove }) _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "post-publish cleanup incomplete") { t.Fatalf("executeStages() error = %v, want incomplete cleanup", err) } assertMissing(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) assertCleanupPending(t, cfg) assertExists(t, seed.otherRunDir) assertExists(t, seed.previousCachePath) removeRunScopedDirFn = originalRemove if _, err := executeStages(context.Background(), cfg, nil, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("retry executeStages() error = %v", err) } assertMissing(t, seed.runWorkDir) assertExists(t, seed.otherRunDir) assertExists(t, seed.previousCachePath) assertCleanupComplete(t, cfg) } func TestPostPublishCleanupRetriesWhenCompletionEvidenceSaveFails(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false failed := false store := &cleanupFailingManifestStore{ delegate: &manifest.LocalStore{}, fail: func(m *manifest.Manifest) error { if m.PostPublishCleanup == nil { return nil } for _, target := range m.PostPublishCleanup.Targets { if !failed && target.Completed { failed = true return errors.New("injected completion evidence failure") } } return nil }, } _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{ Env: &Env{ManifestStore: store, ObjectStore: &storage.FakeBackend{}}, }) if err == nil || !strings.Contains(err.Error(), "post-publish cleanup incomplete") { t.Fatalf("executeStages() error = %v, want incomplete cleanup", err) } assertMissing(t, seed.spoolAudioDir) assertCleanupPending(t, cfg) store.fail = nil if _, err := executeStages(context.Background(), cfg, nil, RunOptions{Env: &Env{ManifestStore: store, ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("retry executeStages() error = %v", err) } assertCleanupComplete(t, cfg) if _, err := executeStages(context.Background(), cfg, nil, RunOptions{Env: &Env{ManifestStore: store, ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("idempotent retry executeStages() error = %v", err) } assertMissing(t, seed.spoolAudioDir) } func TestPostPublishCleanupNotRunWhenPublishFails(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true _, err := executeStages(context.Background(), cfg, []stage.Stage{failingStage{name: "publish", err: errors.New("publish failed")}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "stage \"publish\" failed") { t.Fatalf("executeStages() error = %v, want publish failure", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupNotRunWhenPublishSkipped(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{metadata: map[string]any{"skipped": true}, targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupNotRunWhenCommitPointerMissing(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{metadata: map[string]any{"current_commit_pointer_key": ""}, targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupNotRunWhenPublishUploadDisabled(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true cfg.Pipeline.Publish.UploadRun = boolPtr(false) if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{targets: &seed}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupWaitsUntilAllStagesSucceed(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}, notifyFailStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "stage \"notify\" failed") { t.Fatalf("executeStages() error = %v, want notify failure", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupFailsOnUnsafePath(t *testing.T) { cfg, _ := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false manifestPath := manifestPathFor(cfg) store := &manifest.LocalStore{} m, err := store.Load(context.Background(), manifestPath) if err != nil { t.Fatalf("Load() error = %v", err) } m.MarkStageSucceeded("publish", time.Now().UTC(), nil) m.Stages["publish"].Metadata = map[string]any{ "uploaded": true, "published_run_id": m.RunID, "remote_commit_key": "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260516T010203Z-1a2b3c4d/commit.json", "current_commit_pointer_key": "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/commit-pointer.json", } m.PostPublishCleanup = &manifest.PostPublishCleanup{ CommittedRunID: m.RunID, RemoteCommitKey: m.Stages["publish"].Metadata["remote_commit_key"].(string), CurrentCommitPointerKey: m.Stages["publish"].Metadata["current_commit_pointer_key"].(string), Targets: []manifest.CleanupTarget{{ Policy: "pipeline.spool.delete_audio_after_publish", Root: cfg.Pipeline.Spool.Root, Path: filepath.Join(filepath.Dir(cfg.Pipeline.Spool.Root), "outside-spool"), }}, } if err := store.Save(context.Background(), manifestPath, m); err != nil { t.Fatalf("Save() error = %v", err) } _, err = executeStages(context.Background(), cfg, nil, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "refusing to delete path outside root") { t.Fatalf("executeStages() error = %v, want safe-path failure", err) } } func TestPostPublishCleanupNotRunWhenOutputIsMissing(t *testing.T) { cfg, seed, runID := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true cfg.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } _, err = executeStages(context.Background(), cfg, []stage.Stage{publishStageImpl}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "required output source unavailable") { t.Fatalf("executeStages() error = %v, want required output source unavailable failure", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) assertExists(t, filepath.Join(seed.runWorkDir, "manifest.json")) assertExists(t, artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)) } func TestPostPublishCleanupNotRunWhenCommittedManifestUploadFails(t *testing.T) { cfg, seed, _ := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } _, err = executeStages(context.Background(), cfg, []stage.Stage{publishStageImpl}, RunOptions{ Env: &Env{ObjectStore: &failKeyStore{delegate: &storage.FakeBackend{}, fail: func(key string) bool { return strings.HasSuffix(key, "/session-manifest.json") }}}, }) if err == nil || !strings.Contains(err.Error(), "immutable object") { t.Fatalf("executeStages() error = %v, want committed-manifest failure", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } func TestPostPublishCleanupNotRunWhenCommitPointerUploadFails(t *testing.T) { cfg, seed, _ := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true failKey := artifacts.S3CurrentCommitPointerKey(seed.sessionPrefix) publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } _, err = executeStages(context.Background(), cfg, []stage.Stage{publishStageImpl}, RunOptions{ Env: &Env{ObjectStore: &failKeyStore{delegate: &storage.FakeBackend{}, failKey: failKey}}, }) if err == nil || !strings.Contains(err.Error(), "current commit pointer") { t.Fatalf("executeStages() error = %v, want current-commit-pointer failure", err) } assertExists(t, seed.spoolAudioDir) assertExists(t, seed.runWorkDir) } type cleanupSeed struct { runID string runWorkDir string otherRunDir string spoolAudioDir string localSourceAudio string previousCachePath string sessionPrefix string } type cleanupFailingManifestStore struct { delegate manifest.Store fail func(*manifest.Manifest) error } func (s *cleanupFailingManifestStore) Create(ctx context.Context, sessionID string) (*manifest.Manifest, error) { return s.delegate.Create(ctx, sessionID) } func (s *cleanupFailingManifestStore) Load(ctx context.Context, path string) (*manifest.Manifest, error) { return s.delegate.Load(ctx, path) } func (s *cleanupFailingManifestStore) Save(ctx context.Context, path string, m *manifest.Manifest) error { if s.fail != nil { if err := s.fail(m); err != nil { return err } } return s.delegate.Save(ctx, path, m) } func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) { t.Helper() cfg := testConfig(t) cfg.Pipeline.Publish = &config.PublishConfig{Enabled: boolPtr(true), UploadRun: boolPtr(true)} cfg.Pipeline.Spool.Root = filepath.Join(t.TempDir(), "spool") runID := "20260516T010203Z-1a2b3c4d" runWorkDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID) otherRunDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, "20260516T010204Z-5e6f7a8b") spoolAudioDir := artifacts.SessionSpoolAudioDir(cfg.Pipeline.Spool.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID) previousCachePath := mustPreviousArtifactPathForCampaign(t, cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, "session_recap.md", ) mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n") mustWriteFile(t, filepath.Join(runWorkDir, "logs", "stage.log"), "log\n") mustWriteFile(t, filepath.Join(otherRunDir, "logs", "stage.log"), "other\n") mustWriteFile(t, filepath.Join(spoolAudioDir, "speaker.flac"), "flac\n") mustWriteFile(t, previousCachePath, "# previous recap\n") localSourceAudio := filepath.Join(filepath.Dir(cfg.SessionPath), "audio", "alice.flac") mustWriteFile(t, localSourceAudio, "source\n") seed := manifest.New(cfg.Session.SessionID, time.Now().UTC()) seed.Campaign = cfg.Session.Campaign seed.RunID = runID seed.LocalWorkDir = runWorkDir seed.LocalSpoolDir = spoolAudioDir seed.S3Bucket = "my-dnd-archive" seed.S3SessionPrefix = "dnd/campaigns/sample-campaign/sessions/2026-05-03/" seed.S3RunPrefix = seed.S3SessionPrefix + "runs/" + runID + "/" store := &manifest.LocalStore{} if err := os.MkdirAll(filepath.Dir(manifestPathFor(cfg)), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := store.Save(context.Background(), manifestPathFor(cfg), seed); err != nil { t.Fatalf("seed manifest save error = %v", err) } return cfg, cleanupSeed{ runID: runID, runWorkDir: runWorkDir, otherRunDir: otherRunDir, spoolAudioDir: spoolAudioDir, localSourceAudio: localSourceAudio, previousCachePath: previousCachePath, sessionPrefix: seed.S3SessionPrefix, } } func publishStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, string) { t.Helper() cfg, seed := cleanupFixtureConfig(t) runID := "20260516T010203Z-1a2b3c4d" cfg.Pipeline.Storage.S3 = &config.StorageS3Config{ Bucket: "my-dnd-archive", RootPrefix: "dnd", } cfg.Pipeline.Publish = &config.PublishConfig{ Enabled: boolPtr(true), UploadRun: boolPtr(true), Outputs: []config.PublishOutputRule{ {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, }, } cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{ Artifacts: map[string]config.ScriptoriumArtifactConfig{ "session_recap": { OutputPath: "artifacts/session_recap.md", }, }, } writePublishFixtureRunFiles( t, seed.runWorkDir, artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID), ) store := &manifest.LocalStore{} seedManifest, err := store.Load(context.Background(), manifestPathFor(cfg)) if err != nil { t.Fatalf("Load() error = %v", err) } for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} { seedManifest.MarkStageSucceeded(name, time.Now().UTC(), nil) } seedManifest.S3SessionPrefix = artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.SessionID) seedManifest.S3RunPrefix = artifacts.S3RunPrefix(seedManifest.S3SessionPrefix, runID) if err := store.Save(context.Background(), manifestPathFor(cfg), seedManifest); err != nil { t.Fatalf("Save() error = %v", err) } seed.sessionPrefix = seedManifest.S3SessionPrefix return cfg, seed, runID } func writePublishFixtureRunFiles(t *testing.T, runWorkDir, sessionRoot string) { t.Helper() mustWriteFile(t, filepath.Join(runWorkDir, "prepare", "inputs", "session.yml"), "session_id: 2026-05-03\n") mustWriteFile(t, filepath.Join(runWorkDir, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n") mustWriteFile(t, filepath.Join(runWorkDir, "trim", "outputs", "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") mustWriteFile(t, filepath.Join(runWorkDir, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n") mustWriteFile(t, filepath.Join(runWorkDir, "polish", "reports", "audita.report.json"), "{}\n") mustWriteFile(t, filepath.Join(runWorkDir, "merge", "config", "seriatim.generated.yml"), "key: value\n") mustWriteFile(t, filepath.Join(runWorkDir, "logs", "audita.stderr.log"), "stderr\n") mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n") mustWriteFile(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") mustWriteFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n") } type failKeyStore struct { delegate *storage.FakeBackend failKey string fail func(string) bool } func (s *failKeyStore) fails(key string) bool { return strings.TrimSpace(key) == strings.TrimSpace(s.failKey) || (s.fail != nil && s.fail(key)) } func (s *failKeyStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { return s.delegate.List(ctx, prefix) } func (s *failKeyStore) Read(ctx context.Context, key string) (storage.ObjectInfo, io.ReadCloser, error) { return s.delegate.Read(ctx, key) } func (s *failKeyStore) Download(ctx context.Context, key, localPath string) error { return s.delegate.Download(ctx, key, localPath) } func (s *failKeyStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { if s.fails(key) { return storage.ObjectInfo{}, errors.New("forced upload failure") } return s.delegate.Upload(ctx, localPath, key, opts) } func (s *failKeyStore) UploadReader(ctx context.Context, source io.Reader, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { if s.fails(key) { return storage.ObjectInfo{}, errors.New("forced upload failure") } return s.delegate.UploadReader(ctx, source, key, opts) } func (s *failKeyStore) UploadConditional(ctx context.Context, source io.Reader, key string, opts storage.UploadOptions, condition storage.WriteCondition) (storage.ObjectInfo, error) { if s.fails(key) { return storage.ObjectInfo{}, errors.New("forced upload failure") } return s.delegate.UploadConditional(ctx, source, key, opts, condition) } func (s *failKeyStore) Exists(ctx context.Context, key string) (bool, error) { return s.delegate.Exists(ctx, key) } func assertExists(t *testing.T, path string) { t.Helper() if _, err := os.Stat(path); err != nil { t.Fatalf("expected path to exist %q: %v", path, err) } } func assertMissing(t *testing.T, path string) { t.Helper() if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("expected path to be removed %q, stat err=%v", path, err) } } func assertCleanupPending(t *testing.T, cfg *config.Config) { t.Helper() m, err := (&manifest.LocalStore{}).Load(context.Background(), manifestPathFor(cfg)) if err != nil { t.Fatalf("Load() error = %v", err) } if m.PostPublishCleanup == nil { t.Fatal("expected a persisted cleanup obligation") } for _, target := range m.PostPublishCleanup.Targets { if !target.Completed { return } } t.Fatal("expected at least one cleanup target to remain incomplete") } func assertCleanupComplete(t *testing.T, cfg *config.Config) { t.Helper() m, err := (&manifest.LocalStore{}).Load(context.Background(), manifestPathFor(cfg)) if err != nil { t.Fatalf("Load() error = %v", err) } if m.PostPublishCleanup == nil { t.Fatal("expected a persisted cleanup obligation") } for _, target := range m.PostPublishCleanup.Targets { if !target.Completed { t.Fatalf("cleanup target remains incomplete: %#v", target) } } }