1import { assert, it } from "@effect/vitest";
2import * as Schema from "effect/Schema";
3
4import * as CodexSchema from "./schema.ts";
5
6const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);
7const isThreadReadResponse = Schema.is(CodexSchema.V2ThreadReadResponse);
8const isThreadResumeResponse = Schema.is(CodexSchema.V2ThreadResumeResponse);
9const isThreadForkResponse = Schema.is(CodexSchema.V2ThreadForkResponse);
10const isTurnCompletedNotification = Schema.is(CodexSchema.V2TurnCompletedNotification);
11const decodeThreadResumeResponse = Schema.decodeUnknownSync(CodexSchema.V2ThreadResumeResponse);
12
13it("keeps async questions in live notifications and thread history", () => {
14 const item = {
15 type: "agentMessage",
16 id: "question-1",
17 text: "Which package?\n- pnpm\n- npm\n\nWhat should it be named?",
18 phase: "final_answer",
19 delivery: "async",
20 questions: [
21 { title: "Which package manager?", options: ["pnpm", "npm"] },
22 { title: "What should it be named?" },
23 ],
24 } as const;
25 for (const schema of [
26 CodexSchema.ServerNotification__ThreadItem,
27 CodexSchema.V2ItemStartedNotification__ThreadItem,
28 CodexSchema.V2ItemCompletedNotification__ThreadItem,
29 CodexSchema.V2ThreadReadResponse__ThreadItem,
30 CodexSchema.V2ThreadResumeResponse__ThreadItem,
31 ]) {
32 assert.deepEqual(Schema.decodeSync(schema)(item), item);
33 }
34});
35
36it("accepts Codex 0.150 multi-agent values", () => {
37 const schemas = [
38 CodexSchema.ServerNotification__SubAgentActivityKind,
39 CodexSchema.V2ItemStartedNotification__SubAgentActivityKind,
40 CodexSchema.V2ItemCompletedNotification__SubAgentActivityKind,
41 CodexSchema.V2ThreadReadResponse__SubAgentActivityKind,
42 CodexSchema.V2ThreadResumeResponse__SubAgentActivityKind,
43 ];
44
45 for (const schema of schemas) {
46 assert.equal(Schema.is(schema)("completed"), true);
47 }
48
49 for (const tool of ["sendMessage", "followupTask", "interruptAgent", "listAgents"]) {
50 assert.equal(Schema.is(CodexSchema.ServerNotification__CollabAgentTool)(tool), true);
51 assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse__CollabAgentTool)(tool), true);
52 }
53
54 assert.equal(
55 Schema.is(CodexSchema.ServerNotification__CollabAgentToolCallStatus)("interrupted"),
56 true,
57 );
58 assert.equal(
59 Schema.is(CodexSchema.V2ThreadResumeResponse__CollabAgentToolCallStatus)("interrupted"),
60 true,
61 );
62
63 const resumeResponse = {
64 approvalPolicy: "never",
65 approvalsReviewer: "user",
66 cwd: "/tmp/project",
67 model: "gpt-5.6-sol",
68 modelProvider: "openai",
69 sandbox: { type: "dangerFullAccess" },
70 thread: {
71 cliVersion: "0.150.0",
72 createdAt: 0,
73 cwd: "/tmp/project",
74 ephemeral: false,
75 id: "root-thread",
76 modelProvider: "openai",
77 preview: "",
78 projectId: null,
79 sessionId: "session-1",
80 source: "cli",
81 status: { type: "idle" },
82 turns: [
83 {
84 id: "turn-1",
85 status: "completed",
86 items: [
87 {
88 agentsStates: {},
89 id: "item-1",
90 receiverThreadIds: ["child-thread"],
91 senderThreadId: "root-thread",
92 status: "interrupted",
93 tool: "followupTask",
94 type: "collabAgentToolCall",
95 },
96 ],
97 },
98 ],
99 updatedAt: 0,
100 },
101 };
102
103 assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse)(resumeResponse), true);
104});
105
106it("accepts Codex rate limit errors for thread responses", () => {
107 const failedThread = {
108 cliVersion: "0.150.0",
109 createdAt: 0,
110 cwd: "/tmp/project",
111 ephemeral: false,
112 id: "thread-1",
113 modelProvider: "openai",
114 preview: "",
115 projectId: null,
116 sessionId: "session-1",
117 source: "cli",
118 status: { type: "idle" },
119 turns: [
120 {
121 error: {
122 codexErrorInfo: "rateLimitExceeded",
123 message: "Rate limit exceeded",
124 },
125 id: "turn-1",
126 items: [],
127 status: "failed",
128 },
129 ],
130 updatedAt: 0,
131 };
132 assert.equal(isThreadReadResponse({ thread: failedThread }), true);
133 assert.equal(
134 isThreadResumeResponse({
135 approvalPolicy: "never",
136 approvalsReviewer: "user",
137 cwd: "/tmp/project",
138 model: "gpt-5.6-sol",
139 modelProvider: "openai",
140 sandbox: { type: "dangerFullAccess" },
141 thread: failedThread,
142 }),
143 true,
144 );
145});
146
147it("accepts Codex misalignment policy errors for thread responses", () => {
148 const failedThread = {
149 cliVersion: "0.150.0",
150 createdAt: 0,
151 cwd: "/tmp/project",
152 ephemeral: false,
153 id: "thread-1",
154 modelProvider: "openai",
155 preview: "",
156 projectId: null,
157 sessionId: "session-1",
158 source: "cli",
159 status: { type: "idle" },
160 turns: [
161 {
162 error: {
163 codexErrorInfo: "misalignmentPolicyViolation",
164 message: "Misalignment policy violation",
165 },
166 id: "turn-1",
167 items: [],
168 status: "failed",
169 },
170 ],
171 updatedAt: 0,
172 };
173 const resumeLikeResponse = {
174 approvalPolicy: "never",
175 approvalsReviewer: "user",
176 cwd: "/tmp/project",
177 model: "gpt-5.6-sol",
178 modelProvider: "openai",
179 sandbox: { type: "dangerFullAccess" },
180 thread: failedThread,
181 };
182 assert.equal(isThreadReadResponse({ thread: failedThread }), true);
183 assert.equal(isThreadResumeResponse(resumeLikeResponse), true);
184 assert.equal(isThreadForkResponse(resumeLikeResponse), true);
185 const decodedResume = decodeThreadResumeResponse(resumeLikeResponse);
186 assert.equal(decodedResume.thread.turns[0]?.error?.codexErrorInfo, "misalignmentPolicyViolation");
187 assert.equal(
188 isTurnCompletedNotification({
189 threadId: "thread-1",
190 turn: {
191 error: {
192 codexErrorInfo: "misalignmentPolicyViolation",
193 message: "Misalignment policy violation",
194 },
195 id: "turn-1",
196 items: [],
197 status: "failed",
198 },
199 }),
200 true,
201 );
202});
203
204it("accepts account plan slugs newer than the pinned protocol", () => {
205 const planTypes = [
206 "self_serve_business_prolite",
207 "ent26",
208 "enterprise_cbp_automation",
209 "edu_plus",
210 "edu_pro",
211 "promax",
212 "some_future_plan",
213 ];
214
215 for (const planType of planTypes) {
216 const accountResponse = {
217 account: {
218 email: "user@example.com",
219 planType,
220 type: "chatgpt",
221 },
222 requiresOpenaiAuth: true,
223 };
224
225 assert.equal(isGetAccountResponse(accountResponse), true);
226 }
227});