@blockrun/clawrouter 0.12.91 → 0.12.93
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/dist/cli.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/e2e-multiturn.mjs +156 -0
package/package.json
CHANGED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test: multi-turn chat with continue.dev scenario (issue #135)
|
|
3
|
+
*
|
|
4
|
+
* Simulates exactly what continue.dev does:
|
|
5
|
+
* Turn 1 (new chat) → [system, user]
|
|
6
|
+
* Turn 2 (existing chat) → [system, user1, assistant1, user2]
|
|
7
|
+
*
|
|
8
|
+
* Before fix: Turn 2 always failed with "Unexpected error" from the SSE stream
|
|
9
|
+
* because kimi-k2.5 (primary MEDIUM model) returned 400 for missing
|
|
10
|
+
* reasoning_content on the plain text assistant message.
|
|
11
|
+
* After fix: Both turns succeed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PROXY = "http://localhost:8402";
|
|
15
|
+
// Use free model so test doesn't need a funded wallet
|
|
16
|
+
const MODEL = "blockrun/eco";
|
|
17
|
+
|
|
18
|
+
function log(msg) {
|
|
19
|
+
console.log(`\x1b[36m[e2e]\x1b[0m ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
function ok(msg) {
|
|
22
|
+
console.log(`\x1b[32m✓\x1b[0m ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
function fail(msg) {
|
|
25
|
+
console.error(`\x1b[31m✗\x1b[0m ${msg}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Consume an SSE streaming response and return the reconstructed assistant content.
|
|
31
|
+
* Also detects error events so we can fail fast.
|
|
32
|
+
*/
|
|
33
|
+
async function collectSSE(response) {
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const body = await response.text();
|
|
36
|
+
throw new Error(`HTTP ${response.status}: ${body.slice(0, 200)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reader = response.body.getReader();
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = "";
|
|
42
|
+
let content = "";
|
|
43
|
+
let errorFound = null;
|
|
44
|
+
let done = false;
|
|
45
|
+
|
|
46
|
+
while (!done) {
|
|
47
|
+
const { value, done: streamDone } = await reader.read();
|
|
48
|
+
if (streamDone) break;
|
|
49
|
+
buffer += decoder.decode(value, { stream: true });
|
|
50
|
+
|
|
51
|
+
const lines = buffer.split("\n");
|
|
52
|
+
buffer = lines.pop(); // keep partial line
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.startsWith("data: ")) continue;
|
|
56
|
+
const raw = line.slice(6).trim();
|
|
57
|
+
if (raw === "[DONE]") { done = true; break; }
|
|
58
|
+
|
|
59
|
+
let chunk;
|
|
60
|
+
try {
|
|
61
|
+
chunk = JSON.parse(raw);
|
|
62
|
+
} catch {
|
|
63
|
+
continue; // SSE comments / heartbeats
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Detect error event
|
|
67
|
+
if (chunk.error) {
|
|
68
|
+
errorFound = chunk.error.message || JSON.stringify(chunk.error);
|
|
69
|
+
done = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Accumulate delta content
|
|
74
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
75
|
+
if (delta?.content) content += delta.content;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (errorFound) throw new Error(`SSE error: ${errorFound}`);
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function chatStream(messages) {
|
|
84
|
+
return fetch(`${PROXY}/v1/chat/completions`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
model: MODEL,
|
|
89
|
+
stream: true,
|
|
90
|
+
messages,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Turn 1: new chat ────────────────────────────────────────────────────────
|
|
96
|
+
log("Turn 1 — new chat: [system, user]");
|
|
97
|
+
|
|
98
|
+
const turn1Messages = [
|
|
99
|
+
{ role: "system", content: "You are a helpful assistant. Keep answers brief." },
|
|
100
|
+
{ role: "user", content: "Say exactly: hello" },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
let assistantContent;
|
|
104
|
+
try {
|
|
105
|
+
const resp1 = await chatStream(turn1Messages);
|
|
106
|
+
assistantContent = await collectSSE(resp1);
|
|
107
|
+
if (!assistantContent || assistantContent.trim().length === 0) {
|
|
108
|
+
fail("Turn 1 returned empty content");
|
|
109
|
+
}
|
|
110
|
+
ok(`Turn 1 succeeded. Assistant said: "${assistantContent.trim().slice(0, 80)}"`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
fail(`Turn 1 failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Turn 2: existing chat (the scenario that broke) ─────────────────────────
|
|
116
|
+
log("Turn 2 — existing chat: [system, user1, assistant1, user2]");
|
|
117
|
+
|
|
118
|
+
const turn2Messages = [
|
|
119
|
+
...turn1Messages,
|
|
120
|
+
{ role: "assistant", content: assistantContent }, // ← plain text, no reasoning_content
|
|
121
|
+
{ role: "user", content: "Now say exactly: world" },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
let assistantContent2;
|
|
125
|
+
try {
|
|
126
|
+
const resp2 = await chatStream(turn2Messages);
|
|
127
|
+
assistantContent2 = await collectSSE(resp2);
|
|
128
|
+
if (!assistantContent2 || assistantContent2.trim().length === 0) {
|
|
129
|
+
fail("Turn 2 returned empty content");
|
|
130
|
+
}
|
|
131
|
+
ok(`Turn 2 succeeded. Assistant said: "${assistantContent2.trim().slice(0, 80)}"`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
fail(`Turn 2 failed (issue #135 regression): ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Turn 3: three-turn to be thorough ───────────────────────────────────────
|
|
137
|
+
log("Turn 3 — three-turn: [system, user1, assistant1, user2, assistant2, user3]");
|
|
138
|
+
|
|
139
|
+
const turn3Messages = [
|
|
140
|
+
...turn2Messages,
|
|
141
|
+
{ role: "assistant", content: assistantContent2 },
|
|
142
|
+
{ role: "user", content: "What did you say in your first reply?" },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const resp3 = await chatStream(turn3Messages);
|
|
147
|
+
const assistantContent3 = await collectSSE(resp3);
|
|
148
|
+
if (!assistantContent3 || assistantContent3.trim().length === 0) {
|
|
149
|
+
fail("Turn 3 returned empty content");
|
|
150
|
+
}
|
|
151
|
+
ok(`Turn 3 succeeded. Assistant said: "${assistantContent3.trim().slice(0, 80)}"`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
fail(`Turn 3 failed: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log("\n\x1b[32m✓ All 3 turns passed — multi-turn fix verified\x1b[0m");
|