


























@@ -0,0 +1,262 @@
1+import { describe, expect, it, vi } from "vitest";
2+import { buildCodexMediaUnderstandingProvider } from "./media-understanding-provider.js";
3+import type { CodexAppServerClient } from "./src/app-server/client.js";
4+import type { CodexServerNotification, JsonValue } from "./src/app-server/protocol.js";
5+6+function codexModel(inputModalities: string[] = ["text", "image"]) {
7+return {
8+id: "gpt-5.4",
9+model: "gpt-5.4",
10+upgrade: null,
11+upgradeInfo: null,
12+availabilityNux: null,
13+displayName: "gpt-5.4",
14+description: "GPT-5.4",
15+hidden: false,
16+supportedReasoningEfforts: [{ reasoningEffort: "low", description: "fast" }],
17+defaultReasoningEffort: "low",
18+ inputModalities,
19+supportsPersonality: false,
20+additionalSpeedTiers: [],
21+isDefault: true,
22+};
23+}
24+25+function threadStartResult() {
26+return {
27+thread: {
28+id: "thread-1",
29+forkedFromId: null,
30+preview: "",
31+ephemeral: true,
32+modelProvider: "openai",
33+createdAt: 1,
34+updatedAt: 1,
35+status: { type: "idle" },
36+path: null,
37+cwd: "/tmp/openclaw-agent",
38+cliVersion: "0.118.0",
39+source: "unknown",
40+agentNickname: null,
41+agentRole: null,
42+gitInfo: null,
43+name: null,
44+turns: [],
45+},
46+model: "gpt-5.4",
47+modelProvider: "openai",
48+serviceTier: null,
49+cwd: "/tmp/openclaw-agent",
50+instructionSources: [],
51+approvalPolicy: "never",
52+approvalsReviewer: "user",
53+sandbox: { type: "dangerFullAccess" },
54+permissionProfile: null,
55+reasoningEffort: null,
56+};
57+}
58+59+function turnStartResult(status = "inProgress", items: JsonValue[] = []) {
60+return {
61+turn: {
62+id: "turn-1",
63+ status,
64+ items,
65+error: null,
66+startedAt: null,
67+completedAt: null,
68+durationMs: null,
69+},
70+};
71+}
72+73+function createFakeClient(options?: {
74+inputModalities?: string[];
75+completeWithItems?: boolean;
76+notifyError?: string;
77+}) {
78+const notifications = new Set<(notification: CodexServerNotification) => void>();
79+const requests: Array<{ method: string; params?: JsonValue }> = [];
80+const request = vi.fn(async (method: string, params?: JsonValue) => {
81+requests.push({ method, params });
82+if (method === "model/list") {
83+return {
84+data: [codexModel(options?.inputModalities)],
85+nextCursor: null,
86+};
87+}
88+if (method === "thread/start") {
89+return threadStartResult();
90+}
91+if (method === "turn/start") {
92+if (options?.notifyError) {
93+for (const notify of notifications) {
94+notify({
95+method: "error",
96+params: {
97+threadId: "thread-1",
98+turnId: "turn-1",
99+error: {
100+message: options.notifyError,
101+codexErrorInfo: null,
102+additionalDetails: null,
103+},
104+willRetry: false,
105+},
106+});
107+}
108+} else if (!options?.completeWithItems) {
109+for (const notify of notifications) {
110+notify({
111+method: "item/agentMessage/delta",
112+params: {
113+threadId: "thread-1",
114+turnId: "turn-1",
115+itemId: "msg-1",
116+delta: "A red square.",
117+},
118+});
119+notify({
120+method: "turn/completed",
121+params: {
122+threadId: "thread-1",
123+turnId: "turn-1",
124+turn: turnStartResult("completed").turn,
125+},
126+});
127+}
128+}
129+return turnStartResult(
130+options?.completeWithItems ? "completed" : "inProgress",
131+options?.completeWithItems
132+ ? [
133+{
134+id: "msg-1",
135+type: "agentMessage",
136+text: "A blue circle.",
137+phase: null,
138+memoryCitation: null,
139+},
140+]
141+ : [],
142+);
143+}
144+return {};
145+});
146+147+const client = {
148+ request,
149+addNotificationHandler(handler: (notification: CodexServerNotification) => void) {
150+notifications.add(handler);
151+return () => notifications.delete(handler);
152+},
153+} as unknown as CodexAppServerClient;
154+155+return { client, requests };
156+}
157+158+describe("codex media understanding provider", () => {
159+it("runs image understanding through a bounded Codex app-server turn", async () => {
160+const { client, requests } = createFakeClient();
161+const provider = buildCodexMediaUnderstandingProvider({
162+clientFactory: async () => client,
163+});
164+165+const result = await provider.describeImage?.({
166+buffer: Buffer.from("image-bytes"),
167+fileName: "image.png",
168+mime: "image/png",
169+provider: "codex",
170+model: "gpt-5.4",
171+prompt: "Describe briefly.",
172+timeoutMs: 30_000,
173+cfg: {},
174+agentDir: "/tmp/openclaw-agent",
175+});
176+177+expect(result).toEqual({ text: "A red square.", model: "gpt-5.4" });
178+expect(requests.map((entry) => entry.method)).toEqual([
179+"model/list",
180+"thread/start",
181+"turn/start",
182+]);
183+expect(requests[1]?.params).toMatchObject({
184+model: "gpt-5.4",
185+modelProvider: "openai",
186+approvalPolicy: "never",
187+sandbox: "read-only",
188+dynamicTools: [],
189+ephemeral: true,
190+persistExtendedHistory: false,
191+});
192+expect(requests[2]?.params).toMatchObject({
193+threadId: "thread-1",
194+approvalPolicy: "never",
195+model: "gpt-5.4",
196+input: [
197+{ type: "text", text: "Describe briefly.", text_elements: [] },
198+{ type: "image", url: "data:image/png;base64,aW1hZ2UtYnl0ZXM=" },
199+],
200+});
201+});
202+203+it("extracts text from terminal turn items", async () => {
204+const { client } = createFakeClient({ completeWithItems: true });
205+const provider = buildCodexMediaUnderstandingProvider({
206+clientFactory: async () => client,
207+});
208+209+const result = await provider.describeImages?.({
210+images: [{ buffer: Buffer.from("image-bytes"), fileName: "image.png", mime: "image/png" }],
211+provider: "codex",
212+model: "gpt-5.4",
213+prompt: "Describe briefly.",
214+timeoutMs: 30_000,
215+cfg: {},
216+agentDir: "/tmp/openclaw-agent",
217+});
218+219+expect(result).toEqual({ text: "A blue circle.", model: "gpt-5.4" });
220+});
221+222+it("rejects text-only Codex app-server models before starting a turn", async () => {
223+const { client, requests } = createFakeClient({ inputModalities: ["text"] });
224+const provider = buildCodexMediaUnderstandingProvider({
225+clientFactory: async () => client,
226+});
227+228+await expect(
229+provider.describeImage?.({
230+buffer: Buffer.from("image-bytes"),
231+fileName: "image.png",
232+mime: "image/png",
233+provider: "codex",
234+model: "gpt-5.4",
235+timeoutMs: 30_000,
236+cfg: {},
237+agentDir: "/tmp/openclaw-agent",
238+}),
239+).rejects.toThrow("Codex app-server model does not support images: gpt-5.4");
240+expect(requests.map((entry) => entry.method)).toEqual(["model/list"]);
241+});
242+243+it("surfaces Codex app-server turn errors", async () => {
244+const { client } = createFakeClient({ notifyError: "vision unavailable" });
245+const provider = buildCodexMediaUnderstandingProvider({
246+clientFactory: async () => client,
247+});
248+249+await expect(
250+provider.describeImage?.({
251+buffer: Buffer.from("image-bytes"),
252+fileName: "image.png",
253+mime: "image/png",
254+provider: "codex",
255+model: "gpt-5.4",
256+timeoutMs: 30_000,
257+cfg: {},
258+agentDir: "/tmp/openclaw-agent",
259+}),
260+).rejects.toThrow("vision unavailable");
261+});
262+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。