1import { assert, it } from "@effect/vitest";
2import * as CodexClient from "effect-codex-app-server/client";
3import * as CodexReplay from "effect-codex-app-server/replay";
4import * as Effect from "effect/Effect";
5
6import { revertCodexThread } from "./CodexThreadRevert.ts";
7
8const page = (
9 id: number,
10 cursor: string | null,
11 limit: number,
12 turns: string[],
13 nextCursor: string | null,
14) => [
15 {
16 type: "expect_outbound" as const,
17 frame: {
18 id,
19 method: "thread/turns/list",
20 params: {
21 threadId: "thread",
22 cursor,
23 limit,
24 sortDirection: "desc",
25 itemsView: "summary",
26 },
27 },
28 },
29 {
30 type: "emit_inbound" as const,
31 frame: {
32 id,
33 result: {
34 data: turns.map((id) => ({ id, items: [], status: "completed", error: null })),
35 nextCursor,
36 },
37 },
38 },
39];
40
41it.effect("finds the revert boundary across pages of newest-first turns", () =>
42 Effect.gen(function* () {
43 const client = yield* CodexClient.CodexAppServerClient;
44 const result = yield* revertCodexThread(client, "thread", 3).pipe(Effect.flip);
45 // The final request reaches the server with the third-newest turn, not a turn on the first page.
46 assert.equal(result.message, "boundary reached");
47 }).pipe(
48 Effect.provide(
49 CodexReplay.layerReplay({
50 provider: "codex",
51 protocol: "codex.app-server",
52 version: "test",
53 scenario: "revert-pages",
54 entries: [
55 ...page(1, null, 3, ["newest", "middle"], "older"),
56 ...page(2, "older", 1, ["boundary"], null),
57 {
58 type: "expect_outbound",
59 frame: {
60 id: 3,
61 method: "thread/revert",
62 params: { threadId: "thread", beforeTurnId: "boundary" },
63 },
64 },
65 {
66 type: "emit_inbound",
67 frame: { id: 3, error: { code: -32603, message: "boundary reached" } },
68 },
69 ],
70 }),
71 ),
72 Effect.scoped,
73 ),
74);
75
76it.effect("rejects repeated cursors instead of looping or reverting incomplete history", () =>
77 Effect.gen(function* () {
78 const client = yield* CodexClient.CodexAppServerClient;
79 const result = yield* revertCodexThread(client, "thread", 3).pipe(Effect.flip);
80 assert.equal(result.message, "Thread history pagination repeated a cursor.");
81 }).pipe(
82 Effect.provide(
83 CodexReplay.layerReplay({
84 provider: "codex",
85 protocol: "codex.app-server",
86 version: "test",
87 scenario: "revert-loop",
88 entries: [...page(1, null, 3, ["newest"], "again"), ...page(2, "again", 2, [], "again")],
89 }),
90 ),
91 Effect.scoped,
92 ),
93);