| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/agent" |
| 9 | "reasonix/internal/config" |
| 10 | "reasonix/internal/provider" |
| 11 | "reasonix/internal/session" |
| 12 | ) |
| 13 | |
| 14 | func TestDesktopV5UpgradeExcludesAutomaticRecoveryCopies(t *testing.T) { |
| 15 | isolateDesktopUserDirs(t) |
| 16 | dir := config.SessionDir() |
| 17 | for _, fixture := range []struct { |
| 18 | name string |
| 19 | meta agent.BranchMeta |
| 20 | want bool |
| 21 | }{ |
| 22 | {"ordinary", agent.BranchMeta{}, true}, |
| 23 | {"user-fork", agent.BranchMeta{ParentID: "ordinary"}, true}, |
| 24 | {"my-recovery-notes", agent.BranchMeta{}, true}, |
| 25 | {"marked-copy", agent.BranchMeta{Recovered: true, ParentID: "ordinary"}, false}, |
| 26 | {"version-copy", agent.BranchMeta{VersionKind: agent.VersionRecovery}, false}, |
| 27 | {"digest-copy", agent.BranchMeta{RecoveryDigest: "historical-marker"}, false}, |
| 28 | {"ordinary-recovery-0123456789abcdef", agent.BranchMeta{}, false}, |
| 29 | {"ordinary-recovery-0123456789abcdef-recovery-fedcba9876543210", agent.BranchMeta{}, false}, |
| 30 | } { |
| 31 | path := filepath.Join(dir, fixture.name+".jsonl") |
| 32 | legacy := agent.NewSession("system") |
| 33 | legacy.Add(provider.Message{ID: fixture.name, Role: provider.RoleUser, Content: fixture.name}) |
| 34 | if err := legacy.Save(path); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | writeMigrationJSON(t, agent.BranchMetaPath(path), fixture.meta) |
| 38 | if fixture.name == "ordinary-recovery-0123456789abcdef" { |
| 39 | if err := os.Remove(agent.BranchMetaPath(path)); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | } |
| 43 | if got := desktopMigrationAutomaticRecovery(path); got == fixture.want { |
| 44 | t.Fatalf("unexpected recovery classification: %s = %v", fixture.name, got) |
| 45 | } |
| 46 | } |
| 47 | app := NewApp() |
| 48 | t.Cleanup(app.closeSessionServices) |
| 49 | if err := app.migrateDesktopSessionsV5(t.Context()); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | for _, history := range v5MigrationHistories(t, app) { |
| 53 | last := history[len(history)-1] |
| 54 | if last != "ordinary" && last != "user-fork" && last != "my-recovery-notes" { |
| 55 | t.Fatalf("recovery artifact appeared as a conversation: %v", history) |
| 56 | } |
| 57 | } |
| 58 | assertLineageRestart(t, app, 3) |
| 59 | } |
| 60 | |
| 61 | func TestDesktopV5UpgradeExcludesConvertedRecoveryCopies(t *testing.T) { |
| 62 | for _, removed := range []bool{false, true} { |
| 63 | name := "original-present" |
| 64 | if removed { |
| 65 | name = "original-removed" |
| 66 | } |
| 67 | t.Run(name, func(t *testing.T) { |
| 68 | isolateDesktopUserDirs(t) |
| 69 | path, _, _ := migrationSingleDAGFixture(t) |
| 70 | copyPath := filepath.Join(filepath.Dir(path), "original-v2-recovery-0123456789abcdef.jsonl") |
| 71 | copySession := agent.NewSession("system") |
| 72 | copySession.Add(provider.Message{ID: "copy-only", Role: provider.RoleUser, Content: "automatic conflict data"}) |
| 73 | if err := copySession.Save(copyPath); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | v3root := filepath.Join(filepath.Dir(config.SessionStoreDir()), "sessions-v3") |
| 77 | id := convertedV3Fixture(t, copyPath, v3root, "") |
| 78 | v4, err := session.NewService("fixture", session.NewFilesystemPersistence(config.SessionStoreDir())) |
| 79 | if err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | if _, _, err := v4.ContinuePrototype(t.Context(), filepath.Join(v3root, id)); err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | if err := v4.Shutdown(t.Context()); err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | if removed { |
| 89 | for _, file := range legacyMigrationSourceFiles(copyPath) { |
| 90 | if err := os.Remove(file); err != nil && !os.IsNotExist(err) { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | } |
| 94 | // Only the descendant's archived manifest still knows the origin. |
| 95 | if err := os.RemoveAll(filepath.Join(v3root, id)); err != nil { |
| 96 | t.Fatal(err) |
| 97 | } |
| 98 | } |
| 99 | app := NewApp() |
| 100 | t.Cleanup(app.closeSessionServices) |
| 101 | if err := app.migrateDesktopSessionsV5(t.Context()); err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | assertLineageRestart(t, app, 1) |
| 105 | }) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestDesktopV5UpgradeExcludesRecoveryStoreWithoutTranscript(t *testing.T) { |
| 110 | isolateDesktopUserDirs(t) |
| 111 | root := config.SessionStoreDir() |
| 112 | for _, id := range []string{"ordinary", "only-meta-copy", "ordinary-recovery-0123456789abcdef"} { |
| 113 | service, err := session.NewService("fixture", session.NewFilesystemPersistence(root)) |
| 114 | if err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: id}) |
| 118 | if err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | appendMigrationTestMessage(t, service, runtime.Ref(), id) |
| 122 | if err := service.Shutdown(t.Context()); err != nil { |
| 123 | t.Fatal(err) |
| 124 | } |
| 125 | } |
| 126 | writeMigrationJSON(t, agent.BranchMetaPath(filepath.Join(config.SessionDir(), "only-meta-copy.jsonl")), agent.BranchMeta{Recovered: true}) |
| 127 | app := NewApp() |
| 128 | t.Cleanup(app.closeSessionServices) |
| 129 | if err := app.migrateDesktopSessionsV5(t.Context()); err != nil { |
| 130 | t.Fatal(err) |
| 131 | } |
| 132 | assertLineageRestart(t, app, 1) |
| 133 | } |
| 134 | |
| 135 | func TestDesktopV5UpgradeExcludesArchivedRecoveryMetadata(t *testing.T) { |
| 136 | isolateDesktopUserDirs(t) |
| 137 | migrationSingleDAGFixture(t) |
| 138 | path := filepath.Join(config.SessionDir(), "metadata-marked-copy.jsonl") |
| 139 | legacy := agent.NewSession("system") |
| 140 | legacy.Add(provider.Message{ID: "automatic", Role: provider.RoleUser, Content: "conflict copy"}) |
| 141 | if err := legacy.Save(path); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | writeMigrationJSON(t, agent.BranchMetaPath(path), agent.BranchMeta{Recovered: true}) |
| 145 | root := filepath.Join(filepath.Dir(config.SessionStoreDir()), "sessions-v3") |
| 146 | id := convertedV3Fixture(t, path, root, "") |
| 147 | for _, file := range legacyMigrationSourceFiles(path) { |
| 148 | if err := os.Remove(file); err != nil && !os.IsNotExist(err) { |
| 149 | t.Fatal(err) |
| 150 | } |
| 151 | } |
| 152 | archived := filepath.Join(root, id, "legacy", filepath.Base(path)) |
| 153 | before := migrationSourceSnapshot(t, append(canonicalMigrationSourceFiles(root, id), legacyMigrationSourceFiles(archived)...)) |
| 154 | app := NewApp() |
| 155 | t.Cleanup(app.closeSessionServices) |
| 156 | if err := app.migrateDesktopSessionsV5(t.Context()); err != nil { |
| 157 | t.Fatal(err) |
| 158 | } |
| 159 | assertLineageRestart(t, app, 1) |
| 160 | assertMigrationSourceSnapshot(t, before) |
| 161 | } |
| 162 |