package app import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte(`{"segments":[1,2,3]}`)) seedRestoreObject(fake, sessionPrefix+"artifacts/session_recap.md", []byte("# recap\n")) seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio")) seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) seedRestoreObject(fake, manifestKey, restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)) restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } if !strings.Contains(stdout.String(), "Restored session state for sample-campaign/2026-05-03") { t.Fatalf("stdout = %q, want completion summary", stdout.String()) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "full.json"), `{"segments":[1,2,3]}`) mustReadEquals(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n") reportPath := filepath.Join(sessionRoot, "reports", "restore-latest.json") report := mustReadRestoreReport(t, reportPath) if report.Status != "succeeded" { t.Fatalf("report status = %q, want succeeded", report.Status) } if report.Execution.Downloaded != 3 { t.Fatalf("report execution.downloaded = %d, want 3", report.Execution.Downloaded) } if len(report.Actions) == 0 { t.Fatal("report actions is empty") } if _, err := os.Stat(filepath.Join(sessionRoot, "audio", "alice.flac")); !os.IsNotExist(err) { t.Fatalf("audio should not be restored by default; stat err=%v", err) } } func TestExecuteRestoreRoundTripsPublishedExtractionAndManifestMetadata(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"artifacts/encounters.json", []byte(`{"encounters":[]}`)) seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) remoteManifest := manifest.New(cfg.Session.SessionID, time.Now().UTC()) remoteManifest.Campaign = cfg.Session.Campaign remoteManifest.RunID = "20260519T010203Z-a1b2c3d4" producerRoot := "/prior/workspace/work/sample-campaign/2026-05-03" remoteManifest.LocalWorkDir = filepath.Join(producerRoot, "runs", remoteManifest.RunID) remoteManifest.Stages["extract"] = &manifest.StageRecord{ Name: "extract", Status: manifest.StatusSucceeded, Outputs: []manifest.ArtifactRecord{{ Kind: "notarius_lane", SourceID: artifacts.ExtractionArtifactSourceID("encounters"), LocalPath: filepath.Join(producerRoot, "artifacts", "encounters.json"), Contract: &artifactmodel.ContractMetadata{ MediaType: "application/json", SchemaID: "encounters", SchemaVersion: "1", }, ExternalProvenance: &artifactmodel.ExternalProvenance{ System: "notarius", RunID: "notarius-run-1", PipelineID: "campaign.extract", ArtifactID: "encounters", }, }}, } manifestBody, err := json.Marshal(remoteManifest) if err != nil { t.Fatal(err) } seedRestoreObject(fake, manifestKey, manifestBody) restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustReadEquals(t, filepath.Join(sessionRoot, "artifacts", "encounters.json"), `{"encounters":[]}`) restored, err := (&manifest.LocalStore{}).Load(context.Background(), filepath.Join(sessionRoot, "manifest.json")) if err != nil { t.Fatalf("load restored manifest: %v", err) } lane := restored.Stages["extract"].Outputs[0] if lane.Contract == nil || lane.Contract.SchemaID != "encounters" || lane.ExternalProvenance == nil || lane.ExternalProvenance.RunID != "notarius-run-1" { t.Fatalf("restored extraction metadata = %#v", lane) } if want := filepath.Join(sessionRoot, "artifacts", "encounters.json"); lane.LocalPath != want { t.Fatalf("rebased extraction path = %q, want %q", lane.LocalPath, want) } } func TestExecuteRestoreIncludeAudioRestoresAudio(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio")) restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--include-audio"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustReadEquals(t, filepath.Join(sessionRoot, "audio", "alice.flac"), "remote-audio") report := mustReadRestoreReport(t, filepath.Join(sessionRoot, "reports", "restore-latest.json")) if !report.IncludeAudio { t.Fatalf("report include_audio = %v, want true", report.IncludeAudio) } } func TestExecuteRestoreIncludeAudioUsesCacheAfterWorkspaceDeletion(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) audioKey := sessionPrefix + "audio/alice.flac" seedRestoreObject(fake, audioKey, []byte("remote-audio")) restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--include-audio"}, &stdout, &stderr) if code != 0 { t.Fatalf("first restore exit code = %d, want 0; stderr=%q", code, stderr.String()) } if got := fakeDownloadCount(fake, audioKey); got != 1 { t.Fatalf("audio downloads after first restore = %d, want 1", got) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustReadEquals(t, filepath.Join(sessionRoot, "audio", "alice.flac"), "remote-audio") if err := os.RemoveAll(sessionRoot); err != nil { t.Fatalf("remove session root: %v", err) } stdout.Reset() stderr.Reset() code = Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--include-audio"}, &stdout, &stderr) if code != 0 { t.Fatalf("second restore exit code = %d, want 0; stderr=%q", code, stderr.String()) } if got := fakeDownloadCount(fake, audioKey); got != 1 { t.Fatalf("audio downloads after cached restore = %d, want still 1", got) } mustReadEquals(t, filepath.Join(sessionRoot, "audio", "alice.flac"), "remote-audio") } func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) appendRestoreWorkflowPreviousInputConfig(t, pipelinePath, sessionPath) fake := &storage.FakeBackend{} cfg, _, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestorePreviousCurrent(t, fake, cfg, "# previous recap\n") restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) previousManifestBytes, err := os.ReadFile(filepath.Join(sessionRoot, "previous", "manifest.json")) if err != nil { t.Fatalf("read restored previous manifest: %v", err) } if !strings.Contains(string(previousManifestBytes), `"session_id": "2026-04-26"`) { t.Fatalf("restored previous manifest = %q, want previous session id", string(previousManifestBytes)) } mustReadEquals(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# previous recap\n") report := mustReadRestoreReport(t, filepath.Join(sessionRoot, "reports", "restore-latest.json")) if report.Execution.Downloaded != 3 { t.Fatalf("report execution.downloaded = %d, want 3", report.Execution.Downloaded) } } func TestExecuteRestoreDryRunReportsPreviousCacheWithoutWriting(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) appendRestoreWorkflowPreviousInputConfig(t, pipelinePath, sessionPath) fake := &storage.FakeBackend{} cfg, _, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestorePreviousCurrent(t, fake, cfg, "# previous recap\n") restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--dry-run"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "previous/artifacts/session_recap.md") { t.Fatalf("stdout = %q, want planned previous-cache artifact", stdout.String()) } if !strings.Contains(stdout.String(), "may read remote data; no session files will be written") { t.Fatalf("stdout = %q, want dry-run remote-read notice", stdout.String()) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) if _, err := os.Stat(filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md")); !os.IsNotExist(err) { t.Fatalf("previous artifact should not be written during dry-run; stat err=%v", err) } } func TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript")) sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustWriteTestFile(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "local-transcript") restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "conflicting path") { t.Fatalf("stderr = %q, want conflict failure", stderr.String()) } mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "local-transcript") report := mustReadRestoreReport(t, filepath.Join(sessionRoot, "reports", "restore-latest.json")) if report.Status != "failed" { t.Fatalf("report status = %q, want failed", report.Status) } if report.Plan.Conflicts != 1 { t.Fatalf("report plan.conflicts = %d, want 1", report.Plan.Conflicts) } } func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript")) sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustWriteTestFile(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "local-transcript") restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "remote-transcript") report := mustReadRestoreReport(t, filepath.Join(sessionRoot, "reports", "restore-latest.json")) if !report.Force { t.Fatalf("report force = %v, want true", report.Force) } if _, err := os.Stat(artifacts.SessionRestoreMarkerPathForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)); !os.IsNotExist(err) { t.Fatalf("restore marker should be cleared after success; stat err=%v", err) } } func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) appendRestoreWorkflowPreviousInputConfig(t, pipelinePath, sessionPath) fake := &storage.FakeBackend{} cfg, _, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestorePreviousCurrent(t, fake, cfg, "# remote previous recap\n") sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) mustWriteTestFile(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# local previous recap\n") restoreWithStoreAndRealPhases(t, fake) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } mustReadEquals(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# remote previous recap\n") } func TestExecuteRestoreLockConflictFailsAndWritesNothing(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript")) store := artifacts.NewLocalStore(workspaceRoot) lock, err := store.AcquireSessionLockFor(cfg.Session.Campaign, cfg.Session.SessionID) if err != nil { t.Fatalf("AcquireSessionLockFor() error = %v", err) } defer func() { _ = store.ReleaseSessionLock(lock) }() restoreWithStoreAndRealPhases(t, fake) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() err = Restore(ctx, []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &bytes.Buffer{}) if err == nil || !strings.Contains(err.Error(), "acquire session lock") { t.Fatalf("Restore() error = %v, want lock failure", err) } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) if _, err := os.Stat(filepath.Join(sessionRoot, "transcripts", "full.json")); !os.IsNotExist(err) { t.Fatalf("transcript should not be restored when lock acquisition fails; stat err=%v", err) } } func TestExecuteRestoreInvalidManifestDoesNotCorruptExistingManifest(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) base := &storage.FakeBackend{} cfg, sessionPrefix, manifestKey, _ := seedRestoreCommittedState(t, base, pipelinePath, campaignPath, sessionPath) seedRestoreObject(base, sessionPrefix+"transcripts/full.json", []byte("remote-transcript")) toggled := &stagedManifestDownloadStore{ delegate: base, manifestKey: manifestKey, firstManifest: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign), secondManifest: []byte("{invalid json"), manifestReads: 0, } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) existing := manifest.New(cfg.Session.SessionID, nowUTC()) existing.Campaign = cfg.Session.Campaign existingPath := filepath.Join(sessionRoot, "manifest.json") manifestStore := &manifest.LocalStore{} if err := manifestStore.Save(context.Background(), existingPath, existing); err != nil { t.Fatalf("save existing local manifest: %v", err) } existingData, err := os.ReadFile(existingPath) if err != nil { t.Fatalf("read existing local manifest: %v", err) } restoreWithStoreAndRealPhases(t, toggled) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "restore", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "validate manifest decode") { t.Fatalf("stderr = %q, want manifest validation failure", stderr.String()) } mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "remote-transcript") report := mustReadRestoreReport(t, filepath.Join(sessionRoot, "reports", "restore-latest.json")) if report.Status != "failed" { t.Fatalf("report status = %q, want failed", report.Status) } if strings.TrimSpace(report.Error) == "" { t.Fatal("report error is empty, want failure context") } afterData, err := os.ReadFile(existingPath) if err != nil { t.Fatalf("read local manifest after failure: %v", err) } if string(afterData) != string(existingData) { t.Fatalf("local manifest changed after failed restore; before=%q after=%q", string(existingData), string(afterData)) } if _, err := os.Stat(artifacts.SessionRestoreMarkerPathForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)); err != nil { t.Fatalf("incomplete restore marker should remain after failed forced restore: %v", err) } } func TestExecuteRestorePlanPathMismatchFails(t *testing.T) { cfg := restorePlanConfig(t) current := restorePlanCurrentState(t, cfg) store := &storage.FakeBackend{} seedRestoreObject(store, current.SessionPrefix+"transcripts/full.json", []byte("remote-transcript")) plan := &RestorePlan{Actions: []RestoreAction{{ Kind: RestoreActionDownload, RemoteKey: current.SessionPrefix + "transcripts/full.json", LocalRelativePath: "transcripts/full.json", LocalPath: "/tmp/escape.txt", }}} report, err := newRestoreReport(current, plan, RestorePlanOptions{}) if err != nil { t.Fatalf("newRestoreReport() error = %v", err) } _, err = executeRestorePlan(context.Background(), cfg, current, plan, report, store) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "local path mismatch") { t.Fatalf("error = %v, want local path mismatch", err) } } func mustReadRestoreReport(t *testing.T, path string) *RestoreReport { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile(%q): %v", path, err) } var report RestoreReport if err := json.Unmarshal(data, &report); err != nil { t.Fatalf("Unmarshal restore report %q: %v", path, err) } return &report } func restoreWithStoreAndRealPhases(t *testing.T, objectStore storage.ObjectStore) { t.Helper() origStoreFn := newObjectStoreFromConfigFn origDiscoverFn := discoverRemoteCurrentStateFn origPlanFn := buildRestorePlanFn origExecuteFn := executeRestorePlanFn t.Cleanup(func() { newObjectStoreFromConfigFn = origStoreFn discoverRemoteCurrentStateFn = origDiscoverFn buildRestorePlanFn = origPlanFn executeRestorePlanFn = origExecuteFn }) newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) { return objectStore, nil } discoverRemoteCurrentStateFn = discoverRemoteCurrentState buildRestorePlanFn = buildRestorePlan executeRestorePlanFn = executeRestorePlan } func seedRestoreCommittedState(t *testing.T, fake *storage.FakeBackend, pipelinePath, campaignPath, sessionPath string) (*config.Config, string, string, string) { t.Helper() cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{}) if err != nil { t.Fatalf("LoadWithSessionOptions() error = %v", err) } if err := config.Validate(cfg); err != nil { t.Fatalf("Validate() error = %v", err) } sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) seedRestoreObject(fake, manifestKey, restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)) return cfg, sessionPrefix, manifestKey, runIDKey } func appendRestoreWorkflowPreviousInputConfig(t *testing.T, pipelinePath, sessionPath string) { t.Helper() appendRestoreWorkflowScriptoriumConfig(t, pipelinePath, ` scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: previous_recap: source: narratio.previous_session.artifact.session_recap required: true `) appendRestoreWorkflowScriptoriumConfig(t, sessionPath, ` previous_session_id: 2026-04-26 `) } func seedRestorePreviousCurrent(t *testing.T, fake *storage.FakeBackend, cfg *config.Config, artifactBody string) { t.Helper() seedRestorePreviousCurrentManifestOnly(t, fake, cfg) previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID) seedRestoreObject(fake, previousPrefix+"artifacts/session_recap.md", []byte(artifactBody)) } func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBackend, cfg *config.Config) { t.Helper() previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID) manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix) previousRunID := "20260426T010203Z-a1b2c3d4" seedRestoreObject(fake, runIDKey, []byte(previousRunID+"\n")) m := manifest.New(cfg.Session.PreviousSessionID, nowUTC()) m.Campaign = cfg.Session.Campaign m.RunID = previousRunID data, err := json.Marshal(m) if err != nil { t.Fatalf("marshal previous restore manifest: %v", err) } seedRestoreObject(fake, manifestKey, append(data, '\n')) } func mustReadEquals(t *testing.T, path, want string) { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile(%q): %v", path, err) } if string(data) != want { t.Fatalf("file %q = %q, want %q", path, string(data), want) } } func fakeDownloadCount(fake *storage.FakeBackend, key string) int { count := 0 for _, call := range fake.Downloads { if call.Key == key { count++ } } return count } type stagedManifestDownloadStore struct { delegate *storage.FakeBackend manifestKey string firstManifest []byte secondManifest []byte manifestReads int } func (s *stagedManifestDownloadStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { return s.delegate.List(ctx, prefix) } func (s *stagedManifestDownloadStore) Read(ctx context.Context, key string) (storage.ObjectInfo, io.ReadCloser, error) { return s.delegate.Read(ctx, key) } func (s *stagedManifestDownloadStore) Download(ctx context.Context, key, localPath string) error { if strings.TrimSpace(key) == strings.TrimSpace(s.manifestKey) { s.manifestReads++ payload := s.secondManifest if s.manifestReads <= 1 { payload = s.firstManifest } if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil { return fmt.Errorf("download staged manifest: create parent: %w", err) } if err := os.WriteFile(localPath, payload, 0o644); err != nil { return fmt.Errorf("download staged manifest: write local file: %w", err) } return nil } return s.delegate.Download(ctx, key, localPath) } func (s *stagedManifestDownloadStore) DownloadTo(ctx context.Context, key string, destination io.Writer) error { if strings.TrimSpace(key) == strings.TrimSpace(s.manifestKey) { s.manifestReads++ payload := s.secondManifest if s.manifestReads <= 1 { payload = s.firstManifest } _, err := destination.Write(payload) return err } return storage.DownloadTo(ctx, s.delegate, key, destination) } func (s *stagedManifestDownloadStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { return s.delegate.Upload(ctx, localPath, key, opts) } func (s *stagedManifestDownloadStore) UploadConditional(ctx context.Context, source io.Reader, key string, opts storage.UploadOptions, condition storage.WriteCondition) (storage.ObjectInfo, error) { return s.delegate.UploadConditional(ctx, source, key, opts, condition) } func (s *stagedManifestDownloadStore) Exists(ctx context.Context, key string) (bool, error) { return s.delegate.Exists(ctx, key) }