11import * as NodeAssert from "node:assert/strict" ;
22
3+ import { it } from "@effect/vitest" ;
34import * as Effect from "effect/Effect" ;
45import * as Schema from "effect/Schema" ;
5- import { describe , it } from "vite-plus/test" ;
6+ import { describe } from "vite-plus/test" ;
67import { ThreadId } from "@t3tools/contracts" ;
78import * as CodexErrors from "effect-codex-app-server/errors" ;
89import * as CodexRpc from "effect-codex-app-server/rpc" ;
@@ -19,6 +20,23 @@ import {
1920} from "./CodexSessionRuntime.ts" ;
2021const isCodexAppServerRequestError = Schema . is ( CodexErrors . CodexAppServerRequestError ) ;
2122
23+ describe ( "CodexSessionRuntimeIdentifierGenerationError" , ( ) => {
24+ it ( "retains identifier purpose and the random source failure" , ( ) => {
25+ const cause = new Error ( "random source unavailable" ) ;
26+ const error = new CodexErrors . CodexAppServerIdentifierGenerationError ( {
27+ purpose : "provider-event" ,
28+ cause,
29+ } ) ;
30+
31+ NodeAssert . equal ( error . purpose , "provider-event" ) ;
32+ NodeAssert . strictEqual ( error . cause , cause ) ;
33+ NodeAssert . equal (
34+ error . message ,
35+ "Failed to generate Codex App Server identifier for provider-event." ,
36+ ) ;
37+ } ) ;
38+ } ) ;
39+
2240function makeThreadOpenResponse (
2341 threadId : string ,
2442) : CodexRpc . ClientRequestResponsesByMethod [ "thread/start" ] {
@@ -43,6 +61,32 @@ function makeThreadOpenResponse(
4361}
4462
4563describe ( "buildTurnStartParams" , ( ) => {
64+ it ( "keeps invalid turn values only in the schema cause" , ( ) => {
65+ const secret = "codex-turn-input-secret-sentinel" ;
66+ const error = Effect . runSync (
67+ buildTurnStartParams ( {
68+ threadId : "provider-thread-1" ,
69+ runtimeMode : "full-access" ,
70+ attachments : [
71+ {
72+ type : "image" ,
73+ url : { secret } as unknown as string ,
74+ } ,
75+ ] ,
76+ } ) . pipe ( Effect . flip ) ,
77+ ) ;
78+ const { cause, ...directDiagnostics } = error ;
79+
80+ NodeAssert . equal ( error . operation , "decode-request-payload" ) ;
81+ NodeAssert . equal ( error . method , "turn/start" ) ;
82+ NodeAssert . ok ( ( error . issueCount ?? 0 ) > 0 ) ;
83+ NodeAssert . ok ( error . issueKinds ?. includes ( "Pointer" ) ) ;
84+ NodeAssert . ok ( ( error . maximumPathDepth ?? 0 ) > 0 ) ;
85+ NodeAssert . ok ( Schema . isSchemaError ( cause ) ) ;
86+ NodeAssert . doesNotMatch ( error . message , new RegExp ( secret ) ) ;
87+ NodeAssert . doesNotMatch ( JSON . stringify ( directDiagnostics ) , new RegExp ( secret ) ) ;
88+ } ) ;
89+
4690 it ( "includes plan collaboration mode when requested" , ( ) => {
4791 const params = Effect . runSync (
4892 buildTurnStartParams ( {
@@ -223,81 +267,79 @@ describe("isRecoverableThreadResumeError", () => {
223267} ) ;
224268
225269describe ( "openCodexThread" , ( ) => {
226- it ( "falls back to thread/start when resume fails recoverably" , async ( ) => {
227- const calls : Array < { method : "thread/start" | "thread/resume" ; payload : unknown } > = [ ] ;
228- const started = makeThreadOpenResponse ( "fresh-thread" ) ;
229- const client = {
230- request : < M extends "thread/start" | "thread/resume" > (
231- method : M ,
232- payload : CodexRpc . ClientRequestParamsByMethod [ M ] ,
233- ) => {
234- calls . push ( { method, payload } ) ;
235- if ( method === "thread/resume" ) {
236- return Effect . fail (
237- new CodexErrors . CodexAppServerRequestError ( {
238- code : - 32603 ,
239- errorMessage : "thread not found" ,
240- } ) ,
241- ) ;
242- }
243- return Effect . succeed ( started as CodexRpc . ClientRequestResponsesByMethod [ M ] ) ;
244- } ,
245- } ;
270+ it . effect ( "falls back to thread/start when resume fails recoverably" , ( ) =>
271+ Effect . gen ( function * ( ) {
272+ const calls : Array < { method : "thread/start" | "thread/resume" ; payload : unknown } > = [ ] ;
273+ const started = makeThreadOpenResponse ( "fresh-thread" ) ;
274+ const client = {
275+ request : < M extends "thread/start" | "thread/resume" > (
276+ method : M ,
277+ payload : CodexRpc . ClientRequestParamsByMethod [ M ] ,
278+ ) => {
279+ calls . push ( { method, payload } ) ;
280+ if ( method === "thread/resume" ) {
281+ return Effect . fail (
282+ new CodexErrors . CodexAppServerRequestError ( {
283+ code : - 32603 ,
284+ errorMessage : "thread not found" ,
285+ } ) ,
286+ ) ;
287+ }
288+ return Effect . succeed ( started as CodexRpc . ClientRequestResponsesByMethod [ M ] ) ;
289+ } ,
290+ } ;
246291
247- const opened = await Effect . runPromise (
248- openCodexThread ( {
292+ const opened = yield * openCodexThread ( {
249293 client,
250294 threadId : ThreadId . make ( "thread-1" ) ,
251295 runtimeMode : "full-access" ,
252296 cwd : "/tmp/project" ,
253297 requestedModel : "gpt-5.3-codex" ,
254298 serviceTier : undefined ,
255299 resumeThreadId : "stale-thread" ,
256- } ) ,
257- ) ;
300+ } ) ;
258301
259- NodeAssert . equal ( opened . thread . id , "fresh-thread" ) ;
260- NodeAssert . deepStrictEqual (
261- calls . map ( ( call ) => call . method ) ,
262- [ "thread/resume" , "thread/start" ] ,
263- ) ;
264- } ) ;
302+ NodeAssert . equal ( opened . thread . id , "fresh-thread" ) ;
303+ NodeAssert . deepStrictEqual (
304+ calls . map ( ( call ) => call . method ) ,
305+ [ "thread/resume" , "thread/start" ] ,
306+ ) ;
307+ } ) ,
308+ ) ;
265309
266- it ( "propagates non-recoverable resume failures" , async ( ) => {
267- const client = {
268- request : < M extends "thread/start" | "thread/resume" > (
269- method : M ,
270- _payload : CodexRpc . ClientRequestParamsByMethod [ M ] ,
271- ) => {
272- if ( method === "thread/resume" ) {
273- return Effect . fail (
274- new CodexErrors . CodexAppServerRequestError ( {
275- code : - 32603 ,
276- errorMessage : "timed out waiting for server" ,
277- } ) ,
310+ it . effect ( "propagates non-recoverable resume failures" , ( ) =>
311+ Effect . gen ( function * ( ) {
312+ const client = {
313+ request : < M extends "thread/start" | "thread/resume" > (
314+ method : M ,
315+ _payload : CodexRpc . ClientRequestParamsByMethod [ M ] ,
316+ ) => {
317+ if ( method === "thread/resume" ) {
318+ return Effect . fail (
319+ new CodexErrors . CodexAppServerRequestError ( {
320+ code : - 32603 ,
321+ errorMessage : "timed out waiting for server" ,
322+ } ) ,
323+ ) ;
324+ }
325+ return Effect . succeed (
326+ makeThreadOpenResponse ( "fresh-thread" ) as CodexRpc . ClientRequestResponsesByMethod [ M ] ,
278327 ) ;
279- }
280- return Effect . succeed (
281- makeThreadOpenResponse ( "fresh-thread" ) as CodexRpc . ClientRequestResponsesByMethod [ M ] ,
282- ) ;
283- } ,
284- } ;
328+ } ,
329+ } ;
285330
286- await NodeAssert . rejects (
287- Effect . runPromise (
288- openCodexThread ( {
289- client,
290- threadId : ThreadId . make ( "thread-1" ) ,
291- runtimeMode : "full-access" ,
292- cwd : "/tmp/project" ,
293- requestedModel : "gpt-5.3-codex" ,
294- serviceTier : undefined ,
295- resumeThreadId : "stale-thread" ,
296- } ) ,
297- ) ,
298- ( error : unknown ) =>
299- isCodexAppServerRequestError ( error ) &&
300- error . errorMessage === "timed out waiting for server" ,
301- ) ;
302- } ) ;
331+ const error = yield * openCodexThread ( {
332+ client,
333+ threadId : ThreadId . make ( "thread-1" ) ,
334+ runtimeMode : "full-access" ,
335+ cwd : "/tmp/project" ,
336+ requestedModel : "gpt-5.3-codex" ,
337+ serviceTier : undefined ,
338+ resumeThreadId : "stale-thread" ,
339+ } ) . pipe ( Effect . flip ) ;
340+
341+ NodeAssert . ok ( isCodexAppServerRequestError ( error ) ) ;
342+ NodeAssert . equal ( error . errorMessage , "timed out waiting for server" ) ;
343+ } ) ,
344+ ) ;
303345} ) ;
0 commit comments