| ... | ... | @@ -83,6 +83,7 @@ const runtimeMock = { |
| 83 | 83 | | ((sessionID: string) => Promise<Array<{ id: string }>>) |
| 84 | 84 | | null, |
| 85 | 85 | closeCalls: [] as string[], |
| 86 | revertMessageID: undefined as string | undefined, |
| 86 | 87 | revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, |
| 87 | 88 | messageCalls: [] as Array<{ sessionID: string; messageID: string }>, |
| 88 | 89 | messageFailures: 0, |
| ... | ... | @@ -142,6 +143,7 @@ const runtimeMock = { |
| 142 | 143 | this.state.sessionChildrenById.clear(); |
| 143 | 144 | this.state.sessionChildrenImplementation = null; |
| 144 | 145 | this.state.closeCalls.length = 0; |
| 146 | this.state.revertMessageID = undefined; |
| 145 | 147 | this.state.revertCalls.length = 0; |
| 146 | 148 | this.state.messageCalls.length = 0; |
| 147 | 149 | this.state.messageFailures = 0; |
| ... | ... | @@ -262,6 +264,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { |
| 262 | 264 | return { |
| 263 | 265 | data: { |
| 264 | 266 | id: sessionID, |
| 267 | ...(runtimeMock.state.revertMessageID |
| 268 | ? { revert: { messageID: runtimeMock.state.revertMessageID } } |
| 269 | : {}), |
| 265 | 270 | ...(directory ? { directory } : {}), |
| 266 | 271 | ...(parentID ? { parentID } : {}), |
| 267 | 272 | }, |
| ... | ... | @@ -371,17 +376,16 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { |
| 371 | 376 | ...(messageID ? { messageID } : {}), |
| 372 | 377 | }); |
| 373 | 378 | if (!messageID) { |
| 374 | | runtimeMock.state.messages = []; |
| 375 | | return; |
| 379 | throw new Error("Expected messageID"); |
| 380 | } |
| 381 | let lastUserID: string | undefined; |
| 382 | for (const entry of runtimeMock.state.messages) { |
| 383 | if (entry.info.role === "user") lastUserID = entry.info.id; |
| 384 | if (entry.info.id === messageID && entry.parts.length > 0) { |
| 385 | runtimeMock.state.revertMessageID = lastUserID ?? messageID; |
| 386 | break; |
| 387 | } |
| 376 | 388 | } |
| 377 | | |
| 378 | | const targetIndex = runtimeMock.state.messages.findIndex( |
| 379 | | (entry) => entry.info.id === messageID, |
| 380 | | ); |
| 381 | | runtimeMock.state.messages = |
| 382 | | targetIndex >= 0 |
| 383 | | ? runtimeMock.state.messages.slice(0, targetIndex + 1) |
| 384 | | : runtimeMock.state.messages; |
| 385 | 389 | }, |
| 386 | 390 | }, |
| 387 | 391 | event: { |
| ... | ... | @@ -6316,7 +6320,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 6316 | 6320 | }).pipe(Effect.provide(adapterLayer)); |
| 6317 | 6321 | }); |
| 6318 | 6322 | |
| 6319 | | it.effect("reverts the full thread when rollback removes every assistant turn", () => |
| 6323 | it.effect("reverts the first removed assistant message and returns only retained turns", () => |
| 6320 | 6324 | Effect.gen(function* () { |
| 6321 | 6325 | const adapter = yield* OpenCodeAdapter; |
| 6322 | 6326 | const threadId = asThreadId("thread-rollback-all"); |
| ... | ... | @@ -6327,22 +6331,62 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 6327 | 6331 | }); |
| 6328 | 6332 | |
| 6329 | 6333 | runtimeMock.state.messages = [ |
| 6334 | { info: { id: "user-1", role: "user" }, parts: [] }, |
| 6330 | 6335 | { |
| 6331 | 6336 | info: { id: "assistant-1", role: "assistant" }, |
| 6332 | | parts: [], |
| 6337 | parts: [{ id: "part-1", type: "text", text: "first answer" }], |
| 6333 | 6338 | }, |
| 6339 | { info: { id: "user-2", role: "user" }, parts: [] }, |
| 6334 | 6340 | { |
| 6335 | 6341 | info: { id: "assistant-2", role: "assistant" }, |
| 6336 | | parts: [], |
| 6342 | parts: [{ id: "part-2", type: "text", text: "second answer" }], |
| 6337 | 6343 | }, |
| 6338 | 6344 | ]; |
| 6339 | 6345 | |
| 6340 | | const snapshot = yield* adapter.rollbackThread(threadId, 2); |
| 6341 | | |
| 6342 | | NodeAssert.deepEqual(runtimeMock.state.revertCalls, [ |
| 6343 | | { sessionID: "http://127.0.0.1:9999/session" }, |
| 6344 | | ]); |
| 6345 | | NodeAssert.deepEqual(snapshot.turns, []); |
| 6346 | for (const numTurns of [0, 1, 2, 3]) { |
| 6347 | runtimeMock.state.revertMessageID = undefined; |
| 6348 | runtimeMock.state.revertCalls.length = 0; |
| 6349 | const snapshot = yield* adapter.rollbackThread(threadId, numTurns); |
| 6350 | NodeAssert.deepEqual( |
| 6351 | runtimeMock.state.revertCalls, |
| 6352 | numTurns === 0 |
| 6353 | ? [] |
| 6354 | : [ |
| 6355 | { |
| 6356 | sessionID: "http://127.0.0.1:9999/session", |
| 6357 | messageID: numTurns === 1 ? "assistant-2" : "assistant-1", |
| 6358 | }, |
| 6359 | ], |
| 6360 | ); |
| 6361 | NodeAssert.deepEqual( |
| 6362 | snapshot.turns.map((turn) => turn.id), |
| 6363 | ["assistant-1", "assistant-2"].slice(0, Math.max(0, 2 - numTurns)), |
| 6364 | ); |
| 6365 | } |
| 6366 | runtimeMock.state.revertMessageID = undefined; |
| 6367 | for (const remaining of [1, 0]) { |
| 6368 | const snapshot = yield* adapter.rollbackThread(threadId, 1); |
| 6369 | NodeAssert.equal(snapshot.turns.length, remaining); |
| 6370 | NodeAssert.deepEqual((yield* adapter.readThread(threadId)).turns, snapshot.turns); |
| 6371 | } |
| 6372 | NodeAssert.deepEqual( |
| 6373 | runtimeMock.state.revertCalls.slice(-2).map((call) => call.messageID), |
| 6374 | ["assistant-2", "assistant-1"], |
| 6375 | ); |
| 6376 | runtimeMock.state.revertMessageID = undefined; |
| 6377 | runtimeMock.state.messages = runtimeMock.state.messages.filter( |
| 6378 | (entry) => entry.info.id !== "user-2", |
| 6379 | ); |
| 6380 | const sharedUserSnapshot = yield* adapter.rollbackThread(threadId, 1); |
| 6381 | NodeAssert.equal(runtimeMock.state.revertMessageID, "user-1"); |
| 6382 | NodeAssert.deepEqual(sharedUserSnapshot.turns, []); |
| 6383 | NodeAssert.deepEqual((yield* adapter.readThread(threadId)).turns, []); |
| 6384 | |
| 6385 | runtimeMock.state.messages = []; |
| 6386 | runtimeMock.state.revertCalls.length = 0; |
| 6387 | const emptySnapshot = yield* adapter.rollbackThread(threadId, 1); |
| 6388 | NodeAssert.deepEqual(runtimeMock.state.revertCalls, []); |
| 6389 | NodeAssert.deepEqual(emptySnapshot.turns, []); |
| 6346 | 6390 | }), |
| 6347 | 6391 | ); |
| 6348 | 6392 | |