Implement restore planning for the restore subcommand

This commit is contained in:
2026-05-19 21:58:38 -05:00
parent 128449040f
commit 23d6470b0f
4 changed files with 685 additions and 16 deletions

View File

@@ -37,9 +37,11 @@ func TestExecuteRestoreHelp(t *testing.T) {
func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
origStoreFn := newObjectStoreFromConfigFn
origDiscoverFn := discoverRemoteCurrentStateFn
origPlanFn := buildRestorePlanFn
t.Cleanup(func() {
newObjectStoreFromConfigFn = origStoreFn
discoverRemoteCurrentStateFn = origDiscoverFn
buildRestorePlanFn = origPlanFn
})
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
return &storage.FakeBackend{}, nil
@@ -51,6 +53,19 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
RunID: "20260519T010203Z-a1b2c3d4",
}, nil
}
buildRestorePlanFn = func(context.Context, *config.Config, *RemoteCurrentState, storage.ObjectStore, RestorePlanOptions) (*RestorePlan, error) {
return &RestorePlan{
Actions: []RestoreAction{
{
Kind: RestoreActionDownload,
LocalRelativePath: "manifest.json",
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json",
Reason: "local file missing",
},
},
DownloadCount: 1,
}, nil
}
workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
@@ -70,23 +85,23 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
&stdout,
&stderr,
)
if code == 0 {
t.Fatal("exit code = 0, want non-zero (phase 3 NYI boundary)")
if code != 0 {
t.Fatalf("exit code = %d, want 0 for --dry-run restore planning; stderr=%q", code, stderr.String())
}
errText := stderr.String()
if !strings.Contains(errText, "discovered remote current state for sample-campaign/2026-05-03 (run 20260519T010203Z-a1b2c3d4)") {
t.Fatalf("stderr = %q, want discovery summary context", errText)
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
if !strings.Contains(errText, "not yet implemented (phase 3: remote current-state discovery)") {
t.Fatalf("stderr = %q, want phase-3 NYI marker", errText)
outText := stdout.String()
if !strings.Contains(outText, "restore plan: session sample-campaign/2026-05-03 run=20260519T010203Z-a1b2c3d4") {
t.Fatalf("stdout = %q, want restore plan summary", outText)
}
if strings.Contains(errText, "unknown command") {
t.Fatalf("stderr = %q, restore should be recognized command", errText)
if !strings.Contains(outText, "download manifest.json <- dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json") {
t.Fatalf("stdout = %q, want action output", outText)
}
manifestPath := artifacts.SessionManifestPathForCampaign(workspaceRoot, "sample-campaign", "2026-05-03")
if _, err := os.Stat(manifestPath); !os.IsNotExist(err) {
t.Fatalf("manifest should not be created during phase-3 restore discovery; stat err=%v", err)
t.Fatalf("manifest should not be created during phase-4 restore planning; stat err=%v", err)
}
}
@@ -155,6 +170,97 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
}
}
func TestExecuteRestoreNonDryRunConflictFailsBeforeNYI(t *testing.T) {
origStoreFn := newObjectStoreFromConfigFn
origDiscoverFn := discoverRemoteCurrentStateFn
origPlanFn := buildRestorePlanFn
t.Cleanup(func() {
newObjectStoreFromConfigFn = origStoreFn
discoverRemoteCurrentStateFn = origDiscoverFn
buildRestorePlanFn = origPlanFn
})
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
return &storage.FakeBackend{}, nil
}
discoverRemoteCurrentStateFn = func(context.Context, *config.Config, storage.ObjectStore) (*RemoteCurrentState, error) {
return &RemoteCurrentState{
SessionID: "2026-05-03",
Campaign: "sample-campaign",
RunID: "20260519T010203Z-a1b2c3d4",
}, nil
}
buildRestorePlanFn = func(context.Context, *config.Config, *RemoteCurrentState, storage.ObjectStore, RestorePlanOptions) (*RestorePlan, error) {
return &RestorePlan{
Actions: []RestoreAction{
{Kind: RestoreActionConflict, LocalRelativePath: "transcripts/full.json", RemoteKey: "k", Reason: "local file differs"},
},
ConflictCount: 1,
}, nil
}
workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
if code == 0 {
t.Fatal("exit code = 0, want non-zero")
}
if !strings.Contains(stdout.String(), "restore plan: session sample-campaign/2026-05-03 run=20260519T010203Z-a1b2c3d4") {
t.Fatalf("stdout = %q, want plan output", stdout.String())
}
if !strings.Contains(stderr.String(), "plan has 1 conflicting path(s)") {
t.Fatalf("stderr = %q, want conflict failure", stderr.String())
}
if strings.Contains(stderr.String(), "phase 4: restore execution") {
t.Fatalf("stderr = %q, should fail before phase-4 NYI boundary", stderr.String())
}
}
func TestExecuteRestoreNonDryRunForceStillStopsAtNYI(t *testing.T) {
origStoreFn := newObjectStoreFromConfigFn
origDiscoverFn := discoverRemoteCurrentStateFn
origPlanFn := buildRestorePlanFn
t.Cleanup(func() {
newObjectStoreFromConfigFn = origStoreFn
discoverRemoteCurrentStateFn = origDiscoverFn
buildRestorePlanFn = origPlanFn
})
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
return &storage.FakeBackend{}, nil
}
discoverRemoteCurrentStateFn = func(context.Context, *config.Config, storage.ObjectStore) (*RemoteCurrentState, error) {
return &RemoteCurrentState{
SessionID: "2026-05-03",
Campaign: "sample-campaign",
RunID: "20260519T010203Z-a1b2c3d4",
}, nil
}
buildRestorePlanFn = func(context.Context, *config.Config, *RemoteCurrentState, storage.ObjectStore, RestorePlanOptions) (*RestorePlan, error) {
return &RestorePlan{
Actions: []RestoreAction{
{Kind: RestoreActionDownload, LocalRelativePath: "transcripts/full.json", RemoteKey: "k", Reason: "local file differs; overwrite with --force"},
},
DownloadCount: 1,
}, nil
}
workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--force"}, &stdout, &stderr)
if code == 0 {
t.Fatal("exit code = 0, want non-zero at phase-4 execution boundary")
}
if !strings.Contains(stdout.String(), "restore plan: session sample-campaign/2026-05-03 run=20260519T010203Z-a1b2c3d4") {
t.Fatalf("stdout = %q, want plan output", stdout.String())
}
if !strings.Contains(stderr.String(), "not yet implemented (phase 4: restore execution)") {
t.Fatalf("stderr = %q, want phase-4 NYI marker", stderr.String())
}
}
func writeRestoreConfigWithoutStorage(t *testing.T, workspaceRoot string) (string, string) {
t.Helper()