| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | |
| 11 | "reasonix/internal/agent" |
| 12 | "reasonix/internal/agent/testutil" |
| 13 | "reasonix/internal/event" |
| 14 | "reasonix/internal/provider" |
| 15 | "reasonix/internal/session" |
| 16 | "reasonix/internal/tool" |
| 17 | "reasonix/internal/transcript" |
| 18 | ) |
| 19 | |
| 20 | func TestNativeResumePrefersCompletedCanonicalCutover(t *testing.T) { |
| 21 | root := t.TempDir() |
| 22 | path := filepath.Join(root, "old.jsonl") |
| 23 | if err := os.WriteFile(path, []byte("{\"role\":\"user\",\"content\":\"stale\"}\n"), 0o600); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | service, err := session.NewService("local", session.NewFilesystemPersistence(filepath.Join(root, "canonical"))) |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: agent.BranchID(path)}) |
| 31 | if err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | payload, _ := json.Marshal(map[string]any{"message": provider.Message{ID: "newer", Role: provider.RoleUser, Content: "new canonical work"}}) |
| 35 | if _, err := runtime.Session().Append(t.Context(), session.Batch{OperationID: "newer", Events: []session.Event{{Kind: "message/complete", Payload: payload}}}); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | exec := agent.New(nil, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard) |
| 39 | c := newOwnedTestController(t, Options{Executor: exec, Sink: event.Discard, SessionService: service, NativeLegacySession: true}) |
| 40 | loaded, err := agent.LoadSession(path) |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if err := c.ResumeNativeSession(loaded, path); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | if !c.UsesExclusiveSession() || len(c.History()) != 1 || c.History()[0].Content != "new canonical work" { |
| 48 | t.Fatalf("resumed stale source: %+v", c.History()) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func TestNativeLegacyResumeContinueAndNew(t *testing.T) { |
| 53 | root := t.TempDir() |
| 54 | path := filepath.Join(root, "sessions", "old.jsonl") |
| 55 | if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | original := []byte("{\"role\":\"system\",\"content\":\"system\"}\n{\"role\":\"user\",\"content\":\"old question\"}\n{\"role\":\"assistant\",\"content\":\"old answer\"}\n") |
| 59 | if err := os.WriteFile(path, original, 0o600); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | loaded, err := agent.LoadSession(path) |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | service, err := session.NewService("local", session.NewFilesystemPersistence(filepath.Join(root, "canonical"))) |
| 67 | if err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | exec := agent.New(testutil.NewMock("test", testutil.Turn{Text: "continued answer"}), tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard) |
| 71 | c := newOwnedTestController(t, Options{Runner: exec, Executor: exec, Sink: event.Discard, SystemPrompt: "system", SessionDir: filepath.Dir(path), SessionService: service, NativeLegacySession: true}) |
| 72 | c.Resume(loaded, path) |
| 73 | if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, original) { |
| 74 | t.Fatalf("resume rewrote source: %v", err) |
| 75 | } |
| 76 | if _, err := os.Stat(sessionDirectory(path)); !os.IsNotExist(err) { |
| 77 | t.Fatalf("resume created canonical copy: %v", err) |
| 78 | } |
| 79 | follow, err := c.TranscriptFollow(t.Context(), transcript.FollowRequest{}) |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | if err := c.RunTurn(t.Context(), "continue"); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | if err := c.Snapshot(); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | next, err := c.TranscriptFollow(t.Context(), transcript.FollowRequest{Subscription: follow.Subscription, AfterRevision: follow.Snapshot.ProjectionRevision}) |
| 90 | if err != nil || len(next.Changes) == 0 { |
| 91 | t.Fatalf("legacy follow lost live changes: %+v %v", next, err) |
| 92 | } |
| 93 | _, _ = c.TranscriptFollow(context.Background(), transcript.FollowRequest{Subscription: follow.Subscription, Close: true}) |
| 94 | reopened, err := agent.LoadSession(path) |
| 95 | if err != nil { |
| 96 | t.Fatal(err) |
| 97 | } |
| 98 | messages := reopened.Snapshot() |
| 99 | if last := messages[len(messages)-1]; last.Role != provider.RoleAssistant || last.Content != "continued answer" { |
| 100 | t.Fatalf("continued message missing: %+v", messages) |
| 101 | } |
| 102 | if _, err := os.Stat(sessionDirectory(path)); !os.IsNotExist(err) { |
| 103 | t.Fatalf("continuation created canonical copy: %v", err) |
| 104 | } |
| 105 | if err := c.NewSession(); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | if !c.UsesExclusiveSession() || c.NativeLegacySession() { |
| 109 | t.Fatal("new session retained legacy backend") |
| 110 | } |
| 111 | if _, bound := c.SessionRef(); !bound { |
| 112 | t.Fatal("new session has no canonical identity") |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | func TestNativeDAGHeadIsNotReplacedByAnotherHeadsStore(t *testing.T) { |
| 117 | root := t.TempDir() |
| 118 | path := filepath.Join(root, "branches.jsonl") |
| 119 | loaded := agent.NewSession("system") |
| 120 | loaded.Add(provider.Message{Role: provider.RoleUser, Content: "shared"}) |
| 121 | if err := loaded.Save(path); err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | if _, err := loaded.ForkHead(path, loaded.Snapshot()[1].ID, agent.HeadKindFork, "alternate"); err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | loaded.Add(provider.Message{Role: provider.RoleAssistant, Content: "alternate answer"}) |
| 128 | if err := loaded.Save(path); err != nil { |
| 129 | t.Fatal(err) |
| 130 | } |
| 131 | service, err := session.NewService("local", session.NewFilesystemPersistence(filepath.Join(root, "canonical"))) |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | if _, err := service.Create(t.Context(), session.CreateOptions{SessionID: agent.BranchID(path)}); err != nil { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | exec := agent.New(nil, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard) |
| 139 | c := newOwnedTestController(t, Options{Executor: exec, Sink: event.Discard, SessionService: service, NativeLegacySession: true}) |
| 140 | if err := c.ResumeNativeSession(loaded, path); err != nil { |
| 141 | t.Fatal(err) |
| 142 | } |
| 143 | if !c.NativeLegacySession() || c.History()[2].Content != "alternate answer" { |
| 144 | t.Fatal("a path alias replaced the requested DAG head") |
| 145 | } |
| 146 | } |
| 147 |