From 5be831eb1350a3735ce362d98d4951947c829fe4 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 20 May 2026 15:05:48 +0000 Subject: [PATCH] Restore archived previous-session cache files with session state --- internal/app/restore_execution_test.go | 49 ++++++++++++++++++++++++++ internal/app/restore_plan.go | 3 ++ internal/app/restore_plan_test.go | 21 +++++++++++ 3 files changed, 73 insertions(+) diff --git a/internal/app/restore_execution_test.go b/internal/app/restore_execution_test.go index a761adc..9cca175 100644 --- a/internal/app/restore_execution_test.go +++ b/internal/app/restore_execution_test.go @@ -87,6 +87,33 @@ func TestExecuteRestoreIncludeAudioRestoresAudio(t *testing.T) { } } +func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) + + fake := &storage.FakeBackend{} + cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath) + seedRestoreObject(fake, sessionPrefix+"previous/manifest.json", []byte(`{"session_id":"2026-04-26"}`)) + seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# previous recap\n")) + + restoreWithStoreAndRealPhases(t, fake) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := Execute([]string{"restore", "--config", pipelinePath, "--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, "previous", "manifest.json"), `{"session_id":"2026-04-26"}`) + 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 TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) @@ -145,6 +172,28 @@ func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) { } } +func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) + + fake := &storage.FakeBackend{} + cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath) + seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# 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{"restore", "--config", pipelinePath, "--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, sessionPath := writeValidConfigFiles(t, workspaceRoot) diff --git a/internal/app/restore_plan.go b/internal/app/restore_plan.go index 07ad78e..7e61237 100644 --- a/internal/app/restore_plan.go +++ b/internal/app/restore_plan.go @@ -194,6 +194,9 @@ func restoreLocalRelativePathForKey(sessionPrefix, currentManifestKey, key strin if cleanRel == config.PathArtifactsDirSegment || strings.HasPrefix(cleanRel, config.PathArtifactsDirSegment+"/") { return cleanRel, true, nil } + if cleanRel == config.PathPreviousDirSegment || strings.HasPrefix(cleanRel, config.PathPreviousDirSegment+"/") { + return cleanRel, true, nil + } if includeAudio && (cleanRel == config.PathAudioDirSegment || strings.HasPrefix(cleanRel, config.PathAudioDirSegment+"/")) { return cleanRel, true, nil } diff --git a/internal/app/restore_plan_test.go b/internal/app/restore_plan_test.go index b4eae06..ebd666e 100644 --- a/internal/app/restore_plan_test.go +++ b/internal/app/restore_plan_test.go @@ -58,6 +58,27 @@ func TestRestorePlanIncludeAudio(t *testing.T) { } } +func TestRestorePlanIncludesPreviousCacheByDefault(t *testing.T) { + cfg := restorePlanConfig(t) + current := restorePlanCurrentState(t, cfg) + store := &storage.FakeBackend{} + + seedRestoreObject(store, current.CurrentManifestKey, []byte(`{"session_id":"2026-05-03"}`)) + seedRestoreObject(store, current.SessionPrefix+"previous/manifest.json", []byte(`{"session_id":"2026-04-26"}`)) + seedRestoreObject(store, current.SessionPrefix+"previous/artifacts/session_recap.md", []byte("# previous recap\n")) + + plan, err := buildRestorePlan(context.Background(), cfg, current, store, RestorePlanOptions{}) + if err != nil { + t.Fatalf("buildRestorePlan() error = %v", err) + } + + got := actionRelPaths(plan.Actions) + want := []string{"manifest.json", "previous/artifacts/session_recap.md", "previous/manifest.json"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("action local paths = %#v, want %#v", got, want) + } +} + func TestRestorePlanClassifiesSameAndConflict(t *testing.T) { cfg := restorePlanConfig(t) current := restorePlanCurrentState(t, cfg)