im_wower
·
2026-03-22
index.test.js
1import assert from "node:assert/strict";
2import test from "node:test";
3
4import {
5 CodexAppServerClient,
6 CodexAppServerEventStream,
7 createCodexAppServerReadOnlySandboxPolicy,
8 createCodexAppServerTextInput
9} from "../dist/index.js";
10
11class FakeTransport {
12 constructor(handlersByMethod) {
13 this.handlersByMethod = handlersByMethod;
14 this.requests = [];
15 this.handlers = null;
16 this.closed = false;
17 }
18
19 async connect(handlers) {
20 this.handlers = handlers;
21 }
22
23 async send(message) {
24 const request = JSON.parse(message);
25 this.requests.push(request);
26
27 const handler = this.handlersByMethod[request.method];
28
29 if (typeof handler !== "function") {
30 throw new Error(`Unexpected request method in test transport: ${request.method}`);
31 }
32
33 const plan = await handler(request);
34
35 for (const notification of plan.notifications ?? []) {
36 this.handlers.onMessage(JSON.stringify(notification));
37 }
38
39 if (plan.error) {
40 this.handlers.onMessage(
41 JSON.stringify({
42 id: request.id,
43 error: plan.error
44 })
45 );
46 return;
47 }
48
49 this.handlers.onMessage(
50 JSON.stringify({
51 id: request.id,
52 result: plan.result ?? {}
53 })
54 );
55 }
56
57 async close() {
58 if (this.closed) {
59 return;
60 }
61
62 this.closed = true;
63 this.handlers?.onClose(new Error("closed by test"));
64 }
65}
66
67test("CodexAppServerClient maps app-server methods and notifications into a reusable adapter", async () => {
68 const thread = {
69 id: "thread-1",
70 preview: "hello",
71 ephemeral: true,
72 modelProvider: "openai",
73 createdAt: 1,
74 updatedAt: 2,
75 status: { type: "idle" },
76 cwd: "/tmp/codexd-smoke",
77 cliVersion: "0.116.0",
78 source: { custom: "codexd-test" },
79 name: "smoke",
80 turns: []
81 };
82 const turn = {
83 id: "turn-1",
84 status: "inProgress",
85 error: null
86 };
87 const completedTurn = {
88 ...turn,
89 status: "completed"
90 };
91 const session = {
92 thread,
93 model: "gpt-5.4",
94 modelProvider: "openai",
95 serviceTier: null,
96 cwd: thread.cwd,
97 approvalPolicy: "never",
98 sandbox: createCodexAppServerReadOnlySandboxPolicy(),
99 reasoningEffort: "medium"
100 };
101
102 const transport = new FakeTransport({
103 initialize: async () => ({
104 result: {
105 userAgent: "codex-cli 0.116.0",
106 platformFamily: "unix",
107 platformOs: "macos"
108 }
109 }),
110 "thread/start": async () => ({
111 notifications: [
112 {
113 method: "thread/started",
114 params: { thread }
115 }
116 ],
117 result: session
118 }),
119 "thread/resume": async () => ({
120 result: session
121 }),
122 "turn/start": async () => ({
123 notifications: [
124 {
125 method: "turn/started",
126 params: {
127 threadId: thread.id,
128 turn
129 }
130 },
131 {
132 method: "item/agentMessage/delta",
133 params: {
134 threadId: thread.id,
135 turnId: turn.id,
136 itemId: "item-1",
137 delta: "hel"
138 }
139 },
140 {
141 method: "item/agentMessage/delta",
142 params: {
143 threadId: thread.id,
144 turnId: turn.id,
145 itemId: "item-1",
146 delta: "lo"
147 }
148 },
149 {
150 method: "turn/completed",
151 params: {
152 threadId: thread.id,
153 turn: completedTurn
154 }
155 }
156 ],
157 result: { turn }
158 }),
159 "turn/steer": async () => ({
160 result: {
161 turnId: turn.id
162 }
163 }),
164 "turn/interrupt": async () => ({
165 result: {}
166 })
167 });
168
169 const client = new CodexAppServerClient({
170 clientInfo: {
171 name: "codexd-smoke",
172 title: "smoke",
173 version: "0.1.0"
174 },
175 transport
176 });
177 const receivedEvents = [];
178 const subscription = client.events.subscribe((event) => {
179 receivedEvents.push(event);
180 });
181
182 const initialize = await client.initialize();
183 const startedSession = await client.threadStart({
184 cwd: thread.cwd,
185 baseInstructions: "Be concise."
186 });
187 const resumedSession = await client.threadResume({
188 threadId: thread.id
189 });
190 const startedTurn = await client.turnStart({
191 threadId: thread.id,
192 input: [createCodexAppServerTextInput("Reply with hello.")]
193 });
194 const steeredTurn = await client.turnSteer({
195 threadId: thread.id,
196 expectedTurnId: turn.id,
197 input: [createCodexAppServerTextInput("Reply with hello again.")]
198 });
199
200 await client.turnInterrupt({
201 threadId: thread.id,
202 turnId: turn.id
203 });
204
205 subscription.unsubscribe();
206 await client.close();
207
208 assert.equal(initialize.userAgent, "codex-cli 0.116.0");
209 assert.equal(startedSession.thread.id, thread.id);
210 assert.equal(resumedSession.thread.id, thread.id);
211 assert.equal(startedTurn.turn.id, turn.id);
212 assert.equal(steeredTurn.turnId, turn.id);
213 assert.deepEqual(
214 transport.requests.map((request) => request.method),
215 [
216 "initialize",
217 "thread/start",
218 "thread/resume",
219 "turn/start",
220 "turn/steer",
221 "turn/interrupt"
222 ]
223 );
224 assert.deepEqual(
225 receivedEvents.map((event) => event.type),
226 ["thread.started", "turn.started", "turn.message.delta", "turn.message.delta", "turn.completed"]
227 );
228 assert.equal(receivedEvents[2].delta, "hel");
229 assert.equal(receivedEvents[3].delta, "lo");
230});
231
232test("CodexAppServerEventStream supports async iteration for downstream codexd consumers", async () => {
233 const stream = new CodexAppServerEventStream();
234
235 const iteratorTask = (async () => {
236 const collected = [];
237
238 for await (const event of stream) {
239 collected.push(event);
240
241 if (collected.length === 2) {
242 break;
243 }
244 }
245
246 return collected;
247 })();
248
249 stream.emit({
250 type: "turn.message.delta",
251 notificationMethod: "item/agentMessage/delta",
252 threadId: "thread-1",
253 turnId: "turn-1",
254 itemId: "item-1",
255 delta: "A"
256 });
257 stream.emit({
258 type: "turn.completed",
259 notificationMethod: "turn/completed",
260 threadId: "thread-1",
261 turn: {
262 id: "turn-1",
263 status: "completed",
264 error: null
265 }
266 });
267 stream.close();
268
269 const collected = await iteratorTask;
270
271 assert.deepEqual(
272 collected.map((event) => event.type),
273 ["turn.message.delta", "turn.completed"]
274 );
275});