| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | |
| 4 | import * as CodexSchema from "./schema.ts"; |
| 5 | |
| 6 | const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse); |
| 7 | const isThreadReadResponse = Schema.is(CodexSchema.V2ThreadReadResponse); |
| 8 | const isThreadResumeResponse = Schema.is(CodexSchema.V2ThreadResumeResponse); |
| 9 | const isThreadForkResponse = Schema.is(CodexSchema.V2ThreadForkResponse); |
| 10 | const isTurnCompletedNotification = Schema.is(CodexSchema.V2TurnCompletedNotification); |
| 11 | const decodeThreadResumeResponse = Schema.decodeUnknownSync(CodexSchema.V2ThreadResumeResponse); |
| 12 | |
| 13 | it("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 | |
| 36 | it("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 | |
| 106 | it("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 | |
| 147 | it("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 | |
| 204 | it("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 | }); |