openclaw-codex-app-server 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +22 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/index.ts +69 -0
- package/openclaw.plugin.json +105 -0
- package/package.json +28 -0
- package/src/client.test.ts +332 -0
- package/src/client.ts +2914 -0
- package/src/config.ts +103 -0
- package/src/controller.test.ts +1177 -0
- package/src/controller.ts +3232 -0
- package/src/format.test.ts +502 -0
- package/src/format.ts +869 -0
- package/src/openclaw-plugin-sdk.d.ts +237 -0
- package/src/pending-input.test.ts +298 -0
- package/src/pending-input.ts +785 -0
- package/src/state.test.ts +228 -0
- package/src/state.ts +354 -0
- package/src/thread-picker.test.ts +47 -0
- package/src/thread-picker.ts +98 -0
- package/src/thread-selection.test.ts +89 -0
- package/src/thread-selection.ts +106 -0
- package/src/types.ts +372 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildCodexPlanMarkdownPreview,
|
|
4
|
+
formatBoundThreadSummary,
|
|
5
|
+
formatCodexPlanAttachmentFallback,
|
|
6
|
+
formatCodexPlanAttachmentSummary,
|
|
7
|
+
formatCodexPlanInlineText,
|
|
8
|
+
formatCodexPlanSteps,
|
|
9
|
+
formatCodexReviewFindingMessage,
|
|
10
|
+
formatCodexStatusText,
|
|
11
|
+
formatMcpServers,
|
|
12
|
+
formatModels,
|
|
13
|
+
formatSkills,
|
|
14
|
+
formatThreadPickerIntro,
|
|
15
|
+
formatThreadButtonLabel,
|
|
16
|
+
parseCodexReviewOutput,
|
|
17
|
+
} from "./format.js";
|
|
18
|
+
|
|
19
|
+
describe("formatThreadButtonLabel", () => {
|
|
20
|
+
it("uses worktree and age badges while keeping the project suffix at the end", () => {
|
|
21
|
+
expect(
|
|
22
|
+
formatThreadButtonLabel({
|
|
23
|
+
thread: {
|
|
24
|
+
threadId: "019cdaf5-54be-7ba2-b610-dd71b0efb42b",
|
|
25
|
+
title: "App Server Redux - Plugin Surface Build",
|
|
26
|
+
projectKey: "/Users/huntharo/.codex/worktrees/cb00/openclaw",
|
|
27
|
+
updatedAt: Date.now() - 4 * 60_000,
|
|
28
|
+
createdAt: Date.now() - 10 * 60 * 60_000,
|
|
29
|
+
},
|
|
30
|
+
includeProjectSuffix: true,
|
|
31
|
+
isWorktree: true,
|
|
32
|
+
hasChanges: true,
|
|
33
|
+
}),
|
|
34
|
+
).toContain("🌿 ✏️ App Server Redux - Plugin Surface Build (openclaw) U:4m C:10h");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("falls back to the final workspace segment for non-worktree paths", () => {
|
|
38
|
+
expect(
|
|
39
|
+
formatThreadButtonLabel({
|
|
40
|
+
thread: {
|
|
41
|
+
threadId: "019cbef1-376b-7312-98aa-24488c7499d4",
|
|
42
|
+
projectKey: "/Users/huntharo/.openclaw/workspace",
|
|
43
|
+
},
|
|
44
|
+
includeProjectSuffix: true,
|
|
45
|
+
}),
|
|
46
|
+
).toBe("019cbef1-376b-7312-98aa-24488c7499d4 (workspace)");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("formatBoundThreadSummary", () => {
|
|
51
|
+
it("includes project, thread metadata, and replay context", () => {
|
|
52
|
+
expect(
|
|
53
|
+
formatBoundThreadSummary({
|
|
54
|
+
binding: {
|
|
55
|
+
conversation: {
|
|
56
|
+
channel: "telegram",
|
|
57
|
+
accountId: "default",
|
|
58
|
+
conversationId: "chat-1",
|
|
59
|
+
},
|
|
60
|
+
sessionKey: "openclaw-codex-app-server:thread:abc",
|
|
61
|
+
threadId: "019cc00d-6cf4-7c11-afcd-2673db349a21",
|
|
62
|
+
workspaceDir: "/Users/huntharo/.codex/worktrees/41fb/openclaw",
|
|
63
|
+
threadTitle: "Fix Telegram approval flow",
|
|
64
|
+
updatedAt: 1,
|
|
65
|
+
},
|
|
66
|
+
state: {
|
|
67
|
+
threadId: "019cc00d-6cf4-7c11-afcd-2673db349a21",
|
|
68
|
+
threadName: "Fix Telegram approval flow",
|
|
69
|
+
cwd: "/Users/huntharo/.codex/worktrees/41fb/openclaw",
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
).toBe(
|
|
73
|
+
[
|
|
74
|
+
"Codex thread bound.",
|
|
75
|
+
"Project: openclaw",
|
|
76
|
+
"Thread Name: Fix Telegram approval flow",
|
|
77
|
+
"Thread ID: 019cc00d-6cf4-7c11-afcd-2673db349a21",
|
|
78
|
+
"Worktree Path: /Users/huntharo/.codex/worktrees/41fb/openclaw",
|
|
79
|
+
].join("\n"),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("formatCodexStatusText", () => {
|
|
85
|
+
it("matches the old operational Codex status shape", () => {
|
|
86
|
+
const text = formatCodexStatusText({
|
|
87
|
+
bindingActive: true,
|
|
88
|
+
threadState: {
|
|
89
|
+
threadId: "019cc00d-6cf4-7c11-afcd-2673db349a21",
|
|
90
|
+
threadName: "Fix Telegram approval flow",
|
|
91
|
+
model: "gpt-5.4",
|
|
92
|
+
modelProvider: "openai",
|
|
93
|
+
reasoningEffort: "high",
|
|
94
|
+
serviceTier: "default",
|
|
95
|
+
cwd: "/Users/huntharo/.codex/worktrees/41fb/openclaw",
|
|
96
|
+
approvalPolicy: "on-request",
|
|
97
|
+
sandbox: "workspace-write",
|
|
98
|
+
},
|
|
99
|
+
account: {
|
|
100
|
+
type: "chatgpt",
|
|
101
|
+
email: "huntharo@gmail.com",
|
|
102
|
+
planType: "pro",
|
|
103
|
+
},
|
|
104
|
+
projectFolder: "/Users/huntharo/github/openclaw",
|
|
105
|
+
worktreeFolder: "/Users/huntharo/.codex/worktrees/41fb/openclaw",
|
|
106
|
+
planMode: false,
|
|
107
|
+
rateLimits: [
|
|
108
|
+
{
|
|
109
|
+
name: "5h limit",
|
|
110
|
+
usedPercent: 15,
|
|
111
|
+
resetAt: new Date("2026-03-13T10:03:00-04:00").getTime(),
|
|
112
|
+
windowSeconds: 18_000,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "Weekly limit",
|
|
116
|
+
usedPercent: 15,
|
|
117
|
+
resetAt: new Date("2026-03-14T10:03:00-04:00").getTime(),
|
|
118
|
+
windowSeconds: 604_800,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(text).toContain("OpenAI Codex");
|
|
124
|
+
expect(text).toContain("Binding: active");
|
|
125
|
+
expect(text).toContain("Thread: Fix Telegram approval flow");
|
|
126
|
+
expect(text).toContain("Model: openai/gpt-5.4 · reasoning high");
|
|
127
|
+
expect(text).toContain("Project folder: ~/github/openclaw");
|
|
128
|
+
expect(text).toContain("Worktree folder: ~/.codex/worktrees/41fb/openclaw");
|
|
129
|
+
expect(text).toContain("Fast mode: off");
|
|
130
|
+
expect(text).toContain("Plan mode: off");
|
|
131
|
+
expect(text).toContain("Context usage: unavailable until Codex emits a token-usage update");
|
|
132
|
+
expect(text).toContain("Permissions: Default");
|
|
133
|
+
expect(text).toContain("Account: huntharo@gmail.com (pro)");
|
|
134
|
+
expect(text).toContain("Session: 019cc00d-6cf4-7c11-afcd-2673db349a21");
|
|
135
|
+
expect(text).toContain("Rate limits timezone:");
|
|
136
|
+
expect(text).toContain("5h limit: 85% left");
|
|
137
|
+
expect(text).toContain("Weekly limit: 85% left");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("formats context usage once a fresh token snapshot exists", () => {
|
|
145
|
+
const text = formatCodexStatusText({
|
|
146
|
+
bindingActive: true,
|
|
147
|
+
threadState: {
|
|
148
|
+
threadId: "thread-123",
|
|
149
|
+
threadName: "Plan TASKS doc refresh",
|
|
150
|
+
model: "gpt-5.4",
|
|
151
|
+
modelProvider: "openai",
|
|
152
|
+
reasoningEffort: "high",
|
|
153
|
+
cwd: "/repo/openclaw",
|
|
154
|
+
},
|
|
155
|
+
account: {
|
|
156
|
+
type: "chatgpt",
|
|
157
|
+
email: "user@example.com",
|
|
158
|
+
planType: "pro",
|
|
159
|
+
},
|
|
160
|
+
projectFolder: "/repo/openclaw",
|
|
161
|
+
worktreeFolder: "/repo/openclaw",
|
|
162
|
+
contextUsage: {
|
|
163
|
+
totalTokens: 139_000,
|
|
164
|
+
contextWindow: 258_000,
|
|
165
|
+
},
|
|
166
|
+
rateLimits: [
|
|
167
|
+
{
|
|
168
|
+
name: "5h limit",
|
|
169
|
+
usedPercent: 4,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(text).toContain("Context usage: 139k / 258k tokens used (54% full)");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("shows plan mode on when the bound conversation has an active plan run", () => {
|
|
178
|
+
const text = formatCodexStatusText({
|
|
179
|
+
bindingActive: true,
|
|
180
|
+
threadState: {
|
|
181
|
+
threadId: "thread-123",
|
|
182
|
+
threadName: "Plan TASKS doc refresh",
|
|
183
|
+
model: "gpt-5.4",
|
|
184
|
+
modelProvider: "openai",
|
|
185
|
+
cwd: "/repo/openclaw",
|
|
186
|
+
},
|
|
187
|
+
account: {
|
|
188
|
+
type: "chatgpt",
|
|
189
|
+
email: "user@example.com",
|
|
190
|
+
planType: "pro",
|
|
191
|
+
},
|
|
192
|
+
projectFolder: "/repo/openclaw",
|
|
193
|
+
worktreeFolder: "/repo/openclaw",
|
|
194
|
+
planMode: true,
|
|
195
|
+
rateLimits: [],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(text).toContain("Plan mode: on");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("omits plan mode when the conversation is not bound", () => {
|
|
202
|
+
const text = formatCodexStatusText({
|
|
203
|
+
bindingActive: false,
|
|
204
|
+
account: {
|
|
205
|
+
type: "chatgpt",
|
|
206
|
+
email: "user@example.com",
|
|
207
|
+
planType: "pro",
|
|
208
|
+
},
|
|
209
|
+
projectFolder: "/repo/openclaw",
|
|
210
|
+
worktreeFolder: "/repo/openclaw/workspace",
|
|
211
|
+
rateLimits: [],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(text).not.toContain("Plan mode:");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("does not render a partial context usage line when only the window size is known", () => {
|
|
218
|
+
const text = formatCodexStatusText({
|
|
219
|
+
bindingActive: true,
|
|
220
|
+
threadState: {
|
|
221
|
+
threadId: "thread-123",
|
|
222
|
+
threadName: "Plan TASKS doc refresh",
|
|
223
|
+
model: "gpt-5.4",
|
|
224
|
+
modelProvider: "openai",
|
|
225
|
+
cwd: "/repo/openclaw",
|
|
226
|
+
},
|
|
227
|
+
account: {
|
|
228
|
+
type: "chatgpt",
|
|
229
|
+
email: "user@example.com",
|
|
230
|
+
planType: "pro",
|
|
231
|
+
},
|
|
232
|
+
projectFolder: "/repo/openclaw",
|
|
233
|
+
worktreeFolder: "/repo/openclaw",
|
|
234
|
+
contextUsage: {
|
|
235
|
+
contextWindow: 272_000,
|
|
236
|
+
},
|
|
237
|
+
rateLimits: [],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(text).not.toContain("Context usage: ? / 272k");
|
|
241
|
+
expect(text).toContain("Context usage: unavailable until Codex emits a token-usage update");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("hides non-matching model-specific rate-limit rows", () => {
|
|
245
|
+
const text = formatCodexStatusText({
|
|
246
|
+
bindingActive: true,
|
|
247
|
+
threadState: {
|
|
248
|
+
threadId: "thread-123",
|
|
249
|
+
threadName: "Plan TASKS doc refresh",
|
|
250
|
+
model: "gpt-5.4",
|
|
251
|
+
modelProvider: "openai",
|
|
252
|
+
cwd: "/repo/openclaw",
|
|
253
|
+
},
|
|
254
|
+
account: {
|
|
255
|
+
type: "chatgpt",
|
|
256
|
+
email: "user@example.com",
|
|
257
|
+
planType: "pro",
|
|
258
|
+
},
|
|
259
|
+
projectFolder: "/repo/openclaw",
|
|
260
|
+
worktreeFolder: "/repo/openclaw",
|
|
261
|
+
rateLimits: [
|
|
262
|
+
{ name: "5h limit", usedPercent: 4 },
|
|
263
|
+
{ name: "Weekly limit", usedPercent: 17 },
|
|
264
|
+
{ name: "GPT-5.3-Codex-Spark 5h limit", usedPercent: 0 },
|
|
265
|
+
{ name: "GPT-5.3-Codex-Spark Weekly limit", usedPercent: 0 },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(text).toContain("5h limit: 96% left");
|
|
270
|
+
expect(text).toContain("Weekly limit: 83% left");
|
|
271
|
+
expect(text).not.toContain("GPT-5.3-Codex-Spark 5h limit");
|
|
272
|
+
expect(text).not.toContain("GPT-5.3-Codex-Spark Weekly limit");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("groups model-specific rate-limit rows after generic rows", () => {
|
|
276
|
+
const text = formatCodexStatusText({
|
|
277
|
+
bindingActive: true,
|
|
278
|
+
threadState: {
|
|
279
|
+
threadId: "thread-123",
|
|
280
|
+
threadName: "Plan TASKS doc refresh",
|
|
281
|
+
model: "gpt-5.3-codex-spark",
|
|
282
|
+
modelProvider: "openai",
|
|
283
|
+
cwd: "/repo/openclaw",
|
|
284
|
+
},
|
|
285
|
+
account: {
|
|
286
|
+
type: "chatgpt",
|
|
287
|
+
email: "user@example.com",
|
|
288
|
+
planType: "pro",
|
|
289
|
+
},
|
|
290
|
+
projectFolder: "/repo/openclaw",
|
|
291
|
+
worktreeFolder: "/repo/openclaw",
|
|
292
|
+
rateLimits: [
|
|
293
|
+
{ name: "GPT-5.3-Codex-Spark Weekly limit", usedPercent: 0 },
|
|
294
|
+
{ name: "Weekly limit", usedPercent: 17 },
|
|
295
|
+
{ name: "GPT-5.3-Codex-Spark 5h limit", usedPercent: 0 },
|
|
296
|
+
{ name: "5h limit", usedPercent: 4 },
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const genericFiveHourIndex = text.indexOf("5h limit: 96% left");
|
|
301
|
+
const genericWeeklyIndex = text.indexOf("Weekly limit: 83% left");
|
|
302
|
+
const sparkFiveHourIndex = text.indexOf("GPT-5.3-Codex-Spark 5h limit: 100% left");
|
|
303
|
+
const sparkWeeklyIndex = text.indexOf("GPT-5.3-Codex-Spark Weekly limit: 100% left");
|
|
304
|
+
|
|
305
|
+
expect(genericFiveHourIndex).toBeGreaterThan(-1);
|
|
306
|
+
expect(genericWeeklyIndex).toBeGreaterThan(genericFiveHourIndex);
|
|
307
|
+
expect(sparkFiveHourIndex).toBeGreaterThan(genericWeeklyIndex);
|
|
308
|
+
expect(sparkWeeklyIndex).toBeGreaterThan(sparkFiveHourIndex);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("formats reset windows in local time and rolls stale anchors forward", () => {
|
|
312
|
+
vi.useFakeTimers();
|
|
313
|
+
vi.setSystemTime(new Date("2026-03-07T12:00:00-05:00"));
|
|
314
|
+
|
|
315
|
+
const text = formatCodexStatusText({
|
|
316
|
+
bindingActive: true,
|
|
317
|
+
threadState: {
|
|
318
|
+
threadId: "thread-123",
|
|
319
|
+
threadName: "Plan TASKS doc refresh",
|
|
320
|
+
model: "gpt-5.4",
|
|
321
|
+
modelProvider: "openai",
|
|
322
|
+
cwd: "/repo/openclaw",
|
|
323
|
+
},
|
|
324
|
+
account: {
|
|
325
|
+
type: "chatgpt",
|
|
326
|
+
email: "user@example.com",
|
|
327
|
+
planType: "pro",
|
|
328
|
+
},
|
|
329
|
+
projectFolder: "/repo/openclaw",
|
|
330
|
+
worktreeFolder: "/repo/openclaw",
|
|
331
|
+
rateLimits: [
|
|
332
|
+
{
|
|
333
|
+
name: "5h limit",
|
|
334
|
+
usedPercent: 11,
|
|
335
|
+
resetAt: new Date("2026-01-21T07:28:00-05:00").getTime(),
|
|
336
|
+
windowSeconds: 18_000,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "Weekly limit",
|
|
340
|
+
usedPercent: 20,
|
|
341
|
+
resetAt: new Date("2026-01-21T07:34:00-05:00").getTime(),
|
|
342
|
+
windowSeconds: 604_800,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(text).toContain(
|
|
348
|
+
`Rate limits timezone: ${new Intl.DateTimeFormat().resolvedOptions().timeZone}`,
|
|
349
|
+
);
|
|
350
|
+
expect(text).toContain("5h limit: 89% left (resets 12:28 PM)");
|
|
351
|
+
expect(text).toContain("Weekly limit: 80% left (resets Mar 11)");
|
|
352
|
+
expect(text).not.toContain("Jan 21");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("Codex plan delivery formatting", () => {
|
|
357
|
+
it("builds a truncated markdown preview for large plans", () => {
|
|
358
|
+
const preview = buildCodexPlanMarkdownPreview(`# Plan\n\n${"Long section.\n".repeat(300)}`, 120);
|
|
359
|
+
expect(preview).toContain("[Preview truncated. Open the attachment for the full plan.]");
|
|
360
|
+
expect(preview?.length).toBeGreaterThan(120);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("formats the attachment summary and fallback texts", () => {
|
|
364
|
+
const plan = {
|
|
365
|
+
explanation: "This needs the full rollout guide attached.",
|
|
366
|
+
steps: [{ step: "Write the rollout", status: "inProgress" as const }],
|
|
367
|
+
markdown: `# Plan\n\n${"Long section.\n".repeat(10)}`,
|
|
368
|
+
};
|
|
369
|
+
expect(formatCodexPlanAttachmentSummary(plan)).toContain("Plan preview:");
|
|
370
|
+
expect(formatCodexPlanAttachmentSummary(plan)).not.toContain(
|
|
371
|
+
"The full plan is attached as Markdown.",
|
|
372
|
+
);
|
|
373
|
+
expect(formatCodexPlanAttachmentFallback(plan)).toContain(
|
|
374
|
+
"I couldn't attach the full Markdown plan here, so here's a condensed inline summary instead.",
|
|
375
|
+
);
|
|
376
|
+
expect(formatCodexPlanAttachmentFallback(plan)).toContain("# Plan");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("formatThreadPickerIntro", () => {
|
|
381
|
+
it("includes a legend for resume badges", () => {
|
|
382
|
+
const text = formatThreadPickerIntro({
|
|
383
|
+
page: 0,
|
|
384
|
+
totalPages: 7,
|
|
385
|
+
totalItems: 56,
|
|
386
|
+
includeAll: true,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(text).toContain("Legend: 🌿 worktree, ✏️ uncommitted changes, U updated, C created.");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("mentions topic sync when the resume picker is in sync mode", () => {
|
|
393
|
+
const text = formatThreadPickerIntro({
|
|
394
|
+
page: 0,
|
|
395
|
+
totalPages: 1,
|
|
396
|
+
totalItems: 3,
|
|
397
|
+
includeAll: true,
|
|
398
|
+
syncTopic: true,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(text).toContain("sync the current channel/topic name");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("formatSkills", () => {
|
|
406
|
+
it("matches the old skill summary shape and filtering", () => {
|
|
407
|
+
expect(
|
|
408
|
+
formatSkills({
|
|
409
|
+
workspaceDir: "/repo/openclaw",
|
|
410
|
+
filter: "creator",
|
|
411
|
+
skills: [
|
|
412
|
+
{
|
|
413
|
+
cwd: "/repo/openclaw",
|
|
414
|
+
name: "skill-creator",
|
|
415
|
+
description: "Create or update a Codex skill",
|
|
416
|
+
enabled: true,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
cwd: "/repo/openclaw",
|
|
420
|
+
name: "legacy-helper",
|
|
421
|
+
description: "Old helper",
|
|
422
|
+
enabled: false,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
}),
|
|
426
|
+
).toContain("skill-creator - Create or update a Codex skill");
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("formatMcpServers", () => {
|
|
431
|
+
it("matches the old MCP summary shape", () => {
|
|
432
|
+
expect(
|
|
433
|
+
formatMcpServers({
|
|
434
|
+
servers: [
|
|
435
|
+
{
|
|
436
|
+
name: "github",
|
|
437
|
+
authStatus: "authenticated",
|
|
438
|
+
toolCount: 12,
|
|
439
|
+
resourceCount: 3,
|
|
440
|
+
resourceTemplateCount: 1,
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
}),
|
|
444
|
+
).toContain("github · auth=authenticated · tools=12 · resources=3 · templates=1");
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("formatModels", () => {
|
|
449
|
+
it("shows the current model followed by the available list", () => {
|
|
450
|
+
const text = formatModels(
|
|
451
|
+
[
|
|
452
|
+
{ id: "gpt-5.3-codex", current: true },
|
|
453
|
+
{ id: "gpt-5.2-codex" },
|
|
454
|
+
],
|
|
455
|
+
{
|
|
456
|
+
threadId: "thread-1",
|
|
457
|
+
model: "gpt-5.3-codex",
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(text).toContain("Current model: gpt-5.3-codex");
|
|
462
|
+
expect(text).toContain("Available models:");
|
|
463
|
+
expect(text).toContain("- gpt-5.2-codex");
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe("parseCodexReviewOutput", () => {
|
|
468
|
+
it("parses summary text and structured findings from the old review format", () => {
|
|
469
|
+
const parsed = parseCodexReviewOutput([
|
|
470
|
+
"Looks solid overall.",
|
|
471
|
+
"",
|
|
472
|
+
"[P1] Prefer Stylize helpers Location: /tmp/file.rs:10-20",
|
|
473
|
+
"Use .dim()/.bold() chaining instead of manual Style.",
|
|
474
|
+
"",
|
|
475
|
+
"[P2] Keep helper names consistent Location: /tmp/file.rs:30-35",
|
|
476
|
+
"Rename the helper to match the surrounding naming pattern.",
|
|
477
|
+
].join("\n"));
|
|
478
|
+
|
|
479
|
+
expect(parsed.summary).toBe("Looks solid overall.");
|
|
480
|
+
expect(parsed.findings).toHaveLength(2);
|
|
481
|
+
expect(formatCodexReviewFindingMessage({ finding: parsed.findings[0]!, index: 0 })).toContain(
|
|
482
|
+
"P1\nPrefer Stylize helpers\nLocation: /tmp/file.rs:10-20",
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("formatCodexPlanInlineText", () => {
|
|
488
|
+
it("renders explanation, steps, and markdown for plan output", () => {
|
|
489
|
+
const plan = {
|
|
490
|
+
explanation: "Break the work into safe increments.",
|
|
491
|
+
steps: [
|
|
492
|
+
{ step: "Capture the current behavior", status: "completed" as const },
|
|
493
|
+
{ step: "Patch Telegram delivery", status: "inProgress" as const },
|
|
494
|
+
],
|
|
495
|
+
markdown: "# Plan\n\n- Patch the command",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
expect(formatCodexPlanSteps(plan.steps)).toContain("- [x] Capture the current behavior");
|
|
499
|
+
expect(formatCodexPlanInlineText(plan)).toContain("Break the work into safe increments.");
|
|
500
|
+
expect(formatCodexPlanInlineText(plan)).toContain("# Plan");
|
|
501
|
+
});
|
|
502
|
+
});
|