TestServer_ForkSession exercises the POST /api/sessions/:id/fork endpoint end-to-end: a fork at the Nth user message must return a new session with the history before that message, a fork-numbered title, and a fresh ID. An out-of-range ordinal must be rejected with 400 Bad Request.
(t *testing.T)
| 208 | // title, and a fresh ID. An out-of-range ordinal must be rejected with |
| 209 | // 400 Bad Request. |
| 210 | func TestServer_ForkSession(t *testing.T) { |
| 211 | t.Parallel() |
| 212 | |
| 213 | ctx := t.Context() |
| 214 | store := session.NewInMemorySessionStore() |
| 215 | |
| 216 | parent := session.New() |
| 217 | parent.Title = "Original" |
| 218 | parent.Messages = []session.Item{ |
| 219 | session.NewMessageItem(session.UserMessage("hello")), |
| 220 | session.NewMessageItem(session.NewAgentMessage("root", &chat.Message{ |
| 221 | Role: chat.MessageRoleAssistant, |
| 222 | Content: "hi there", |
| 223 | })), |
| 224 | session.NewMessageItem(session.UserMessage("ignore me")), |
| 225 | } |
| 226 | require.NoError(t, store.AddSession(ctx, parent)) |
| 227 | |
| 228 | lnPath := startServerWithStore(t, ctx, prepareAgentsDir(t), store) |
| 229 | |
| 230 | // Happy path: fork before the second user message (ordinal 1). |
| 231 | resp := httpDo(t, ctx, http.MethodPost, lnPath, |
| 232 | "/api/sessions/"+parent.ID+"/fork", |
| 233 | api.ForkSessionRequest{UserMessageIndex: 1}) |
| 234 | var forked api.SessionResponse |
| 235 | unmarshal(t, resp, &forked) |
| 236 | |
| 237 | assert.NotEqual(t, parent.ID, forked.ID) |
| 238 | assert.Equal(t, "Original (fork 1)", forked.Title) |
| 239 | require.Len(t, forked.Messages, 2) |
| 240 | assert.Equal(t, "hello", forked.Messages[0].Message.Content) |
| 241 | assert.Equal(t, "hi there", forked.Messages[1].Message.Content) |
| 242 | |
| 243 | // Fork must be persisted server-side so a subsequent GET returns it. |
| 244 | getResp := httpGET(t, ctx, lnPath, "/api/sessions/"+forked.ID) |
| 245 | var fetched api.SessionResponse |
| 246 | unmarshal(t, getResp, &fetched) |
| 247 | assert.Equal(t, forked.ID, fetched.ID) |
| 248 | assert.Equal(t, "Original (fork 1)", fetched.Title) |
| 249 | |
| 250 | // Forking past the last user message (no "full clone" shortcut) must |
| 251 | // return 400, not 500. This pins the sentinel-driven classification so |
| 252 | // future error-message reshuffles can't silently flip the status code. |
| 253 | outOfRange := httpRaw(t, ctx, http.MethodPost, lnPath, |
| 254 | "/api/sessions/"+parent.ID+"/fork", |
| 255 | api.ForkSessionRequest{UserMessageIndex: 99}) |
| 256 | assert.Equal(t, http.StatusBadRequest, outOfRange.StatusCode, outOfRange.body) |
| 257 | } |
| 258 | |
| 259 | // httpRaw issues an HTTP request and returns the raw response without |
| 260 | // asserting on the status code, so tests can verify 4xx/5xx paths. |
nothing calls this directly
no test coverage detected