Skip to content

Commit e46f9d8

Browse files
committed
test(ai): add live Responses stream coverage
1 parent 82b807c commit e46f9d8

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Type } from "typebox";
2+
import { describe, expect, it } from "vitest";
3+
import type { Context, Model } from "../types.js";
4+
import { streamOpenAIResponses } from "./openai-responses.js";
5+
6+
// Live coverage for the Responses stream state machine: real streams interleave
7+
// reasoning/message/tool items, so unit fakes alone cannot prove slot tracking,
8+
// terminal-event handling, or the max_output_tokens floor against the real API.
9+
const LIVE = process.env.OPENCLAW_LIVE_TEST === "1";
10+
const OPENAI_KEY = process.env.OPENAI_API_KEY ?? "";
11+
const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip;
12+
13+
const LIVE_MODEL_ID = process.env.OPENCLAW_LIVE_RESPONSES_MODEL || "gpt-5.6-luna";
14+
const LIVE_TIMEOUT_MS = 120_000;
15+
16+
function liveModel(overrides: Partial<Model<"openai-responses">> = {}) {
17+
return {
18+
id: LIVE_MODEL_ID,
19+
name: LIVE_MODEL_ID,
20+
api: "openai-responses",
21+
provider: "openai",
22+
baseUrl: "https://api.openai.com/v1",
23+
reasoning: true,
24+
input: ["text"],
25+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
26+
contextWindow: 200_000,
27+
maxTokens: 8192,
28+
...overrides,
29+
} satisfies Model<"openai-responses">;
30+
}
31+
32+
describeLive("OpenAI Responses live", () => {
33+
it(
34+
"streams a reply through a terminal event with usage accounting",
35+
async () => {
36+
const context: Context = {
37+
messages: [{ role: "user", content: "Reply with exactly: OK", timestamp: 0 }],
38+
};
39+
const result = await streamOpenAIResponses(liveModel(), context, {
40+
apiKey: OPENAI_KEY,
41+
maxTokens: 256,
42+
}).result();
43+
44+
expect(result.errorMessage).toBeUndefined();
45+
expect(result.stopReason).toBe("stop");
46+
const text = result.content
47+
.filter((block) => block.type === "text")
48+
.map((block) => block.text)
49+
.join("");
50+
expect(text).toContain("OK");
51+
expect(result.usage.totalTokens).toBeGreaterThan(0);
52+
expect(result.usage.output).toBeGreaterThan(0);
53+
},
54+
LIVE_TIMEOUT_MS,
55+
);
56+
57+
it(
58+
"clamps a sub-floor max token budget instead of failing the request",
59+
async () => {
60+
const context: Context = {
61+
messages: [{ role: "user", content: "Reply with exactly: OK", timestamp: 0 }],
62+
};
63+
const result = await streamOpenAIResponses(liveModel(), context, {
64+
apiKey: OPENAI_KEY,
65+
maxTokens: 1,
66+
}).result();
67+
68+
// Pre-floor behavior was a hard 400 from the API; truncation is expected.
69+
expect(result.errorMessage).toBeUndefined();
70+
expect(["stop", "length"]).toContain(result.stopReason);
71+
},
72+
LIVE_TIMEOUT_MS,
73+
);
74+
75+
it(
76+
"keeps reasoning and tool-call items separable on a real interleaved stream",
77+
async () => {
78+
const context: Context = {
79+
messages: [
80+
{
81+
role: "user",
82+
content: "Call the live_probe tool with value set to exactly LIVE_OK.",
83+
timestamp: 0,
84+
},
85+
],
86+
tools: [
87+
{
88+
name: "live_probe",
89+
description: "Records a probe value.",
90+
parameters: Type.Object({ value: Type.String() }),
91+
},
92+
],
93+
};
94+
const result = await streamOpenAIResponses(liveModel(), context, {
95+
apiKey: OPENAI_KEY,
96+
maxTokens: 1024,
97+
reasoningEffort: "low",
98+
}).result();
99+
100+
expect(result.errorMessage).toBeUndefined();
101+
expect(result.stopReason).toBe("toolUse");
102+
const toolCalls = result.content.filter((block) => block.type === "toolCall");
103+
expect(toolCalls).toHaveLength(1);
104+
expect(toolCalls[0]?.name).toBe("live_probe");
105+
expect((toolCalls[0]?.arguments as { value?: string }).value).toBe("LIVE_OK");
106+
for (const block of result.content) {
107+
if (block.type === "thinking") {
108+
expect(block.thinkingSignature).toBeTruthy();
109+
}
110+
}
111+
},
112+
LIVE_TIMEOUT_MS,
113+
);
114+
});

test/vitest/vitest.live.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ export default defineConfig({
2727
[...(baseTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"].map(resolveRepoRootPath),
2828
),
2929
],
30-
include: ["src/**/*.live.test.ts", "test/**/*.live.test.ts", BUNDLED_PLUGIN_LIVE_TEST_GLOB],
30+
include: [
31+
"src/**/*.live.test.ts",
32+
"test/**/*.live.test.ts",
33+
"packages/*/src/**/*.live.test.ts",
34+
BUNDLED_PLUGIN_LIVE_TEST_GLOB,
35+
],
3136
exclude,
3237
},
3338
});

0 commit comments

Comments
 (0)