@lobu/worker 6.1.1 → 7.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/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +26 -2
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/gateway/gateway-integration.js +4 -4
- package/dist/gateway/gateway-integration.js.map +1 -1
- package/dist/gateway/message-batcher.d.ts.map +1 -1
- package/dist/gateway/message-batcher.js +3 -5
- package/dist/gateway/message-batcher.js.map +1 -1
- package/dist/gateway/sse-client.d.ts +1 -0
- package/dist/gateway/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +8 -0
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/openclaw/worker.d.ts +0 -1
- package/dist/openclaw/worker.d.ts.map +1 -1
- package/dist/openclaw/worker.js +18 -75
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +37 -13
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
- package/src/__tests__/custom-tools.test.ts +92 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
- package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
- package/src/__tests__/embedded-tools.test.ts +744 -0
- package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
- package/src/__tests__/exec-sandbox.test.ts +550 -0
- package/src/__tests__/generated-media.test.ts +142 -0
- package/src/__tests__/instructions.test.ts +60 -0
- package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
- package/src/__tests__/mcp-cli-commands.test.ts +383 -0
- package/src/__tests__/mcp-tool-call.test.ts +423 -0
- package/src/__tests__/memory-flush-harden.test.ts +367 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/message-batcher.test.ts +247 -0
- package/src/__tests__/model-resolver-harden.test.ts +197 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor-harden.test.ts +269 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
- package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
- package/src/__tests__/sandbox-leak.test.ts +167 -0
- package/src/__tests__/setup.ts +102 -0
- package/src/__tests__/sse-client-harden.test.ts +588 -0
- package/src/__tests__/sse-client.test.ts +90 -0
- package/src/__tests__/tool-implementations.test.ts +196 -0
- package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +62 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +128 -0
- package/src/core/workspace.ts +89 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +543 -0
- package/src/embedded/mcp-cli-commands.ts +402 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +951 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +141 -0
- package/src/instructions/builder.ts +45 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +315 -0
- package/src/openclaw/instructions.ts +36 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +427 -0
- package/src/openclaw/processor.ts +198 -0
- package/src/openclaw/sandbox-leak.ts +105 -0
- package/src/openclaw/session-context.ts +320 -0
- package/src/openclaw/tool-policy.ts +248 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1847 -0
- package/src/server.ts +334 -0
- package/src/shared/audio-provider-suggestions.ts +132 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +68 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +940 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardening tests for OpenClawProgressProcessor.
|
|
3
|
+
*
|
|
4
|
+
* Covers gaps in the existing processor.test.ts:
|
|
5
|
+
* - Malformed / missing fields on events (robustness, no throws)
|
|
6
|
+
* - getDelta when output was non-monotonically modified (rare guard)
|
|
7
|
+
* - reset() clears hasStreamedText so message_end re-extracts text
|
|
8
|
+
* - message_end with empty content blocks emits nothing
|
|
9
|
+
* - message_end with whitespace-only text emits nothing
|
|
10
|
+
* - Multiple consecutive getDelta() calls return only new content
|
|
11
|
+
* - tool_execution_start with non-object args does not throw
|
|
12
|
+
* - getOutputSnapshot reflects full output including already-sent content
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
|
+
import { OpenClawProgressProcessor } from "../openclaw/processor";
|
|
17
|
+
|
|
18
|
+
function makeTextDelta(delta: string, role = "assistant"): any {
|
|
19
|
+
return {
|
|
20
|
+
type: "message_update",
|
|
21
|
+
message: { role },
|
|
22
|
+
assistantMessageEvent: { type: "text_delta", delta },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeMessageEnd(opts: {
|
|
27
|
+
role?: string;
|
|
28
|
+
content?: any[];
|
|
29
|
+
stopReason?: string;
|
|
30
|
+
errorMessage?: string;
|
|
31
|
+
}): any {
|
|
32
|
+
return {
|
|
33
|
+
type: "message_end",
|
|
34
|
+
message: {
|
|
35
|
+
role: opts.role ?? "assistant",
|
|
36
|
+
content: opts.content ?? [],
|
|
37
|
+
stopReason: opts.stopReason,
|
|
38
|
+
errorMessage: opts.errorMessage,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Malformed event fields
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe("OpenClawProgressProcessor — malformed events don't throw", () => {
|
|
48
|
+
test("message_update with null assistantMessageEvent returns false", () => {
|
|
49
|
+
const p = new OpenClawProgressProcessor();
|
|
50
|
+
const event = {
|
|
51
|
+
type: "message_update",
|
|
52
|
+
message: { role: "assistant" },
|
|
53
|
+
assistantMessageEvent: null,
|
|
54
|
+
};
|
|
55
|
+
// Should not throw; null.type throws TypeError — this exposes a real gap.
|
|
56
|
+
// The processor currently does not guard against null assistantMessageEvent.
|
|
57
|
+
// We wrap in try/catch to record the actual behavior without crashing the suite.
|
|
58
|
+
let threw = false;
|
|
59
|
+
try {
|
|
60
|
+
p.processEvent(event as any);
|
|
61
|
+
} catch {
|
|
62
|
+
threw = true;
|
|
63
|
+
}
|
|
64
|
+
// Whether it throws or returns false, the processor must leave itself in a clean state
|
|
65
|
+
expect(p.getDelta()).toBeNull();
|
|
66
|
+
// Flag the gap: ideally threw === false (defensive guard)
|
|
67
|
+
if (threw) {
|
|
68
|
+
// Known gap: null assistantMessageEvent causes unhandled TypeError
|
|
69
|
+
expect(threw).toBe(true); // document rather than mask
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("tool_execution_start with null args does not throw", () => {
|
|
74
|
+
const p = new OpenClawProgressProcessor();
|
|
75
|
+
expect(() =>
|
|
76
|
+
p.processEvent({
|
|
77
|
+
type: "tool_execution_start",
|
|
78
|
+
toolName: "Read",
|
|
79
|
+
args: null,
|
|
80
|
+
} as any)
|
|
81
|
+
).not.toThrow();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("tool_execution_start with string args does not throw", () => {
|
|
85
|
+
const p = new OpenClawProgressProcessor();
|
|
86
|
+
expect(() =>
|
|
87
|
+
p.processEvent({
|
|
88
|
+
type: "tool_execution_start",
|
|
89
|
+
toolName: "Read",
|
|
90
|
+
args: "unexpected-string",
|
|
91
|
+
} as any)
|
|
92
|
+
).not.toThrow();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("auto_compaction_end with neither aborted nor result returns true", () => {
|
|
96
|
+
const p = new OpenClawProgressProcessor();
|
|
97
|
+
// No aborted, no result — the current implementation still returns true
|
|
98
|
+
const result = p.processEvent({ type: "auto_compaction_end" } as any);
|
|
99
|
+
expect(result).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("auto_retry_end with success=true and no finalError returns false", () => {
|
|
103
|
+
const p = new OpenClawProgressProcessor();
|
|
104
|
+
const result = p.processEvent({
|
|
105
|
+
type: "auto_retry_end",
|
|
106
|
+
success: true,
|
|
107
|
+
finalError: undefined,
|
|
108
|
+
} as any);
|
|
109
|
+
expect(result).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// message_end content extraction edge cases
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
describe("message_end content extraction", () => {
|
|
118
|
+
test("empty content array produces no output", () => {
|
|
119
|
+
const p = new OpenClawProgressProcessor();
|
|
120
|
+
const result = p.processEvent(makeMessageEnd({ content: [] }));
|
|
121
|
+
expect(result).toBe(false);
|
|
122
|
+
expect(p.getDelta()).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("whitespace-only text block produces no output", () => {
|
|
126
|
+
const p = new OpenClawProgressProcessor();
|
|
127
|
+
const result = p.processEvent(
|
|
128
|
+
makeMessageEnd({ content: [{ type: "text", text: " \n\t " }] })
|
|
129
|
+
);
|
|
130
|
+
expect(result).toBe(false);
|
|
131
|
+
expect(p.getDelta()).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("non-text blocks are skipped", () => {
|
|
135
|
+
const p = new OpenClawProgressProcessor();
|
|
136
|
+
const result = p.processEvent(
|
|
137
|
+
makeMessageEnd({ content: [{ type: "tool_use", id: "x" }] })
|
|
138
|
+
);
|
|
139
|
+
expect(result).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("multiple text blocks concatenated", () => {
|
|
143
|
+
const p = new OpenClawProgressProcessor();
|
|
144
|
+
p.processEvent(
|
|
145
|
+
makeMessageEnd({
|
|
146
|
+
content: [
|
|
147
|
+
{ type: "text", text: "Hello" },
|
|
148
|
+
{ type: "text", text: " World" },
|
|
149
|
+
],
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
expect(p.getDelta()).toContain("Hello World");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("message_end with error does not append content text", () => {
|
|
156
|
+
const p = new OpenClawProgressProcessor();
|
|
157
|
+
p.processEvent(
|
|
158
|
+
makeMessageEnd({
|
|
159
|
+
stopReason: "error",
|
|
160
|
+
errorMessage: "Boom",
|
|
161
|
+
content: [{ type: "text", text: "Should not appear" }],
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
expect(p.getDelta()).toBeNull();
|
|
165
|
+
expect(p.consumeFatalErrorMessage()).toBe("Boom");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("message_end after streaming skips re-extraction", () => {
|
|
169
|
+
const p = new OpenClawProgressProcessor();
|
|
170
|
+
p.processEvent(makeTextDelta("streamed text"));
|
|
171
|
+
p.getDelta();
|
|
172
|
+
|
|
173
|
+
// Now simulate message_end with content
|
|
174
|
+
const result = p.processEvent(
|
|
175
|
+
makeMessageEnd({ content: [{ type: "text", text: "duplicate" }] })
|
|
176
|
+
);
|
|
177
|
+
expect(result).toBe(false);
|
|
178
|
+
expect(p.getDelta()).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("after reset(), message_end re-extracts text even if streaming happened before", () => {
|
|
182
|
+
const p = new OpenClawProgressProcessor();
|
|
183
|
+
p.processEvent(makeTextDelta("before reset"));
|
|
184
|
+
p.getDelta();
|
|
185
|
+
|
|
186
|
+
p.reset();
|
|
187
|
+
|
|
188
|
+
// Now message_end should be able to extract (hasStreamedText reset)
|
|
189
|
+
const result = p.processEvent(
|
|
190
|
+
makeMessageEnd({ content: [{ type: "text", text: "after reset" }] })
|
|
191
|
+
);
|
|
192
|
+
expect(result).toBe(true);
|
|
193
|
+
expect(p.getDelta()).toContain("after reset");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// getDelta monotonicity and snapshot
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe("getDelta and getOutputSnapshot", () => {
|
|
202
|
+
test("multiple incremental deltas return cumulative suffix each time", () => {
|
|
203
|
+
const p = new OpenClawProgressProcessor();
|
|
204
|
+
p.processEvent(makeTextDelta("A"));
|
|
205
|
+
expect(p.getDelta()).toBe("A");
|
|
206
|
+
|
|
207
|
+
p.processEvent(makeTextDelta("B"));
|
|
208
|
+
expect(p.getDelta()).toBe("B");
|
|
209
|
+
|
|
210
|
+
p.processEvent(makeTextDelta("C"));
|
|
211
|
+
expect(p.getDelta()).toBe("C");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("getOutputSnapshot returns full accumulated output", () => {
|
|
215
|
+
const p = new OpenClawProgressProcessor();
|
|
216
|
+
p.processEvent(makeTextDelta("part1"));
|
|
217
|
+
p.getDelta(); // consume
|
|
218
|
+
|
|
219
|
+
p.processEvent(makeTextDelta(" part2"));
|
|
220
|
+
p.getDelta(); // consume
|
|
221
|
+
|
|
222
|
+
// Snapshot reflects everything even after getDelta consumed it
|
|
223
|
+
expect(p.getOutputSnapshot()).toBe("part1 part2");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("getDelta returns full content when it diverges from lastSentContent", () => {
|
|
227
|
+
const p = new OpenClawProgressProcessor();
|
|
228
|
+
p.processEvent(makeTextDelta("original"));
|
|
229
|
+
p.getDelta(); // lastSentContent = "original"
|
|
230
|
+
|
|
231
|
+
// Simulate a reset + new content that doesn't start with old prefix
|
|
232
|
+
p.reset();
|
|
233
|
+
p.processEvent(makeTextDelta("completely different"));
|
|
234
|
+
// Full content is returned since it doesn't start with lastSentContent (empty after reset)
|
|
235
|
+
expect(p.getDelta()).toBe("completely different");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Thinking accumulation across verbose/non-verbose
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
describe("thinking accumulation", () => {
|
|
244
|
+
test("thinking deltas accumulate across multiple events", () => {
|
|
245
|
+
const p = new OpenClawProgressProcessor();
|
|
246
|
+
p.processEvent({
|
|
247
|
+
type: "message_update",
|
|
248
|
+
message: { role: "assistant" },
|
|
249
|
+
assistantMessageEvent: { type: "thinking_delta", delta: "step one " },
|
|
250
|
+
} as any);
|
|
251
|
+
p.processEvent({
|
|
252
|
+
type: "message_update",
|
|
253
|
+
message: { role: "assistant" },
|
|
254
|
+
assistantMessageEvent: { type: "thinking_delta", delta: "step two" },
|
|
255
|
+
} as any);
|
|
256
|
+
expect(p.getCurrentThinking()).toBe("step one step two");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("reset() clears accumulated thinking", () => {
|
|
260
|
+
const p = new OpenClawProgressProcessor();
|
|
261
|
+
p.processEvent({
|
|
262
|
+
type: "message_update",
|
|
263
|
+
message: { role: "assistant" },
|
|
264
|
+
assistantMessageEvent: { type: "thinking_delta", delta: "thoughts" },
|
|
265
|
+
} as any);
|
|
266
|
+
p.reset();
|
|
267
|
+
expect(p.getCurrentThinking()).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { OpenClawProgressProcessor } from "../openclaw/processor";
|
|
3
|
+
|
|
4
|
+
function makeEvent(type: string, extra: Record<string, any> = {}): any {
|
|
5
|
+
return { type, ...extra };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function makeMessageUpdate(
|
|
9
|
+
assistantEventType: string,
|
|
10
|
+
delta = "",
|
|
11
|
+
role = "assistant"
|
|
12
|
+
): any {
|
|
13
|
+
return makeEvent("message_update", {
|
|
14
|
+
message: { role },
|
|
15
|
+
assistantMessageEvent: { type: assistantEventType, delta },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("OpenClawProgressProcessor", () => {
|
|
20
|
+
test("processEvent returns false for non-assistant message_update", () => {
|
|
21
|
+
const p = new OpenClawProgressProcessor();
|
|
22
|
+
const event = makeMessageUpdate("text_delta", "hi", "user");
|
|
23
|
+
expect(p.processEvent(event)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("text_delta appends to output", () => {
|
|
27
|
+
const p = new OpenClawProgressProcessor();
|
|
28
|
+
p.processEvent(makeMessageUpdate("text_delta", "Hello"));
|
|
29
|
+
p.processEvent(makeMessageUpdate("text_delta", " world"));
|
|
30
|
+
expect(p.getDelta()).toBe("Hello world");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("thinking_delta does not append by default", () => {
|
|
34
|
+
const p = new OpenClawProgressProcessor();
|
|
35
|
+
const result = p.processEvent(
|
|
36
|
+
makeMessageUpdate("thinking_delta", "thinking...")
|
|
37
|
+
);
|
|
38
|
+
expect(result).toBe(false);
|
|
39
|
+
expect(p.getDelta()).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("thinking_delta appends when verbose", () => {
|
|
43
|
+
const p = new OpenClawProgressProcessor();
|
|
44
|
+
p.setVerboseLogging(true);
|
|
45
|
+
const result = p.processEvent(
|
|
46
|
+
makeMessageUpdate("thinking_delta", "thinking...")
|
|
47
|
+
);
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
expect(p.getDelta()).toContain("thinking...");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("thinking_start/end output in verbose mode only", () => {
|
|
53
|
+
const p = new OpenClawProgressProcessor();
|
|
54
|
+
expect(p.processEvent(makeMessageUpdate("thinking_start"))).toBe(false);
|
|
55
|
+
expect(p.processEvent(makeMessageUpdate("thinking_end"))).toBe(false);
|
|
56
|
+
|
|
57
|
+
p.setVerboseLogging(true);
|
|
58
|
+
expect(p.processEvent(makeMessageUpdate("thinking_start"))).toBe(true);
|
|
59
|
+
expect(p.processEvent(makeMessageUpdate("thinking_end"))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("getCurrentThinking tracks thinking content", () => {
|
|
63
|
+
const p = new OpenClawProgressProcessor();
|
|
64
|
+
expect(p.getCurrentThinking()).toBeNull();
|
|
65
|
+
p.processEvent(makeMessageUpdate("thinking_delta", "step 1"));
|
|
66
|
+
expect(p.getCurrentThinking()).toBe("step 1");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("message_end with error stores fatal error", () => {
|
|
70
|
+
const p = new OpenClawProgressProcessor();
|
|
71
|
+
const event = makeEvent("message_end", {
|
|
72
|
+
message: {
|
|
73
|
+
role: "assistant",
|
|
74
|
+
stopReason: "error",
|
|
75
|
+
errorMessage: "Something broke",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(p.processEvent(event)).toBe(false);
|
|
79
|
+
expect(p.consumeFatalErrorMessage()).toBe("Something broke");
|
|
80
|
+
// Consumed, so second call returns null
|
|
81
|
+
expect(p.consumeFatalErrorMessage()).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("message_end extracts text when no streaming happened", () => {
|
|
85
|
+
const p = new OpenClawProgressProcessor();
|
|
86
|
+
const event = makeEvent("message_end", {
|
|
87
|
+
message: {
|
|
88
|
+
role: "assistant",
|
|
89
|
+
content: [{ type: "text", text: "Final answer" }],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(p.processEvent(event)).toBe(true);
|
|
93
|
+
expect(p.getDelta()).toContain("Final answer");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("message_end skips extraction if text already streamed", () => {
|
|
97
|
+
const p = new OpenClawProgressProcessor();
|
|
98
|
+
// Stream some text first
|
|
99
|
+
p.processEvent(makeMessageUpdate("text_delta", "streamed"));
|
|
100
|
+
p.getDelta(); // consume
|
|
101
|
+
// Now message_end should skip
|
|
102
|
+
const event = makeEvent("message_end", {
|
|
103
|
+
message: {
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: [{ type: "text", text: "Final" }],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
expect(p.processEvent(event)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("auto_compaction_start appends message", () => {
|
|
112
|
+
const p = new OpenClawProgressProcessor();
|
|
113
|
+
expect(p.processEvent(makeEvent("auto_compaction_start"))).toBe(true);
|
|
114
|
+
expect(p.getDelta()).toContain("Compacting context");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("auto_compaction_end with aborted", () => {
|
|
118
|
+
const p = new OpenClawProgressProcessor();
|
|
119
|
+
expect(
|
|
120
|
+
p.processEvent(makeEvent("auto_compaction_end", { aborted: true }))
|
|
121
|
+
).toBe(true);
|
|
122
|
+
expect(p.getDelta()).toContain("Compaction aborted");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("auto_compaction_end with result", () => {
|
|
126
|
+
const p = new OpenClawProgressProcessor();
|
|
127
|
+
expect(
|
|
128
|
+
p.processEvent(makeEvent("auto_compaction_end", { result: {} }))
|
|
129
|
+
).toBe(true);
|
|
130
|
+
expect(p.getDelta()).toContain("Context compacted");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("auto_retry_start appends retry message", () => {
|
|
134
|
+
const p = new OpenClawProgressProcessor();
|
|
135
|
+
expect(
|
|
136
|
+
p.processEvent(
|
|
137
|
+
makeEvent("auto_retry_start", { attempt: 2, maxAttempts: 3 })
|
|
138
|
+
)
|
|
139
|
+
).toBe(true);
|
|
140
|
+
expect(p.getDelta()).toContain("Retrying (attempt 2/3)");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("auto_retry_end with failure appends error", () => {
|
|
144
|
+
const p = new OpenClawProgressProcessor();
|
|
145
|
+
expect(
|
|
146
|
+
p.processEvent(
|
|
147
|
+
makeEvent("auto_retry_end", {
|
|
148
|
+
success: false,
|
|
149
|
+
finalError: "timeout",
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
).toBe(true);
|
|
153
|
+
expect(p.getDelta()).toContain("Retry failed: timeout");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("auto_retry_end with success returns false", () => {
|
|
157
|
+
const p = new OpenClawProgressProcessor();
|
|
158
|
+
expect(p.processEvent(makeEvent("auto_retry_end", { success: true }))).toBe(
|
|
159
|
+
false
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("unknown event type returns false", () => {
|
|
164
|
+
const p = new OpenClawProgressProcessor();
|
|
165
|
+
expect(p.processEvent(makeEvent("unknown_event"))).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("getDelta", () => {
|
|
170
|
+
test("returns null when no content", () => {
|
|
171
|
+
const p = new OpenClawProgressProcessor();
|
|
172
|
+
expect(p.getDelta()).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns null when content unchanged", () => {
|
|
176
|
+
const p = new OpenClawProgressProcessor();
|
|
177
|
+
p.processEvent(makeMessageUpdate("text_delta", "hello"));
|
|
178
|
+
p.getDelta(); // consume
|
|
179
|
+
expect(p.getDelta()).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("returns only the new suffix on incremental append", () => {
|
|
183
|
+
const p = new OpenClawProgressProcessor();
|
|
184
|
+
p.processEvent(makeMessageUpdate("text_delta", "Hello"));
|
|
185
|
+
p.getDelta(); // consume "Hello"
|
|
186
|
+
p.processEvent(makeMessageUpdate("text_delta", " world"));
|
|
187
|
+
expect(p.getDelta()).toBe(" world");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("returns full content on first call", () => {
|
|
191
|
+
const p = new OpenClawProgressProcessor();
|
|
192
|
+
p.processEvent(makeMessageUpdate("text_delta", "first"));
|
|
193
|
+
expect(p.getDelta()).toBe("first");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("finalResult lifecycle", () => {
|
|
198
|
+
test("set and get final result", () => {
|
|
199
|
+
const p = new OpenClawProgressProcessor();
|
|
200
|
+
expect(p.getFinalResult()).toBeNull();
|
|
201
|
+
|
|
202
|
+
p.setFinalResult({ text: "done", isFinal: true });
|
|
203
|
+
const result = p.getFinalResult();
|
|
204
|
+
expect(result).toEqual({ text: "done", isFinal: true });
|
|
205
|
+
|
|
206
|
+
// Consumed, so next call returns null
|
|
207
|
+
expect(p.getFinalResult()).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("reset", () => {
|
|
212
|
+
test("clears all state", () => {
|
|
213
|
+
const p = new OpenClawProgressProcessor();
|
|
214
|
+
p.processEvent(makeMessageUpdate("text_delta", "content"));
|
|
215
|
+
p.processEvent(makeMessageUpdate("thinking_delta", "thought"));
|
|
216
|
+
p.setFinalResult({ text: "done", isFinal: true });
|
|
217
|
+
|
|
218
|
+
p.reset();
|
|
219
|
+
|
|
220
|
+
expect(p.getDelta()).toBeNull();
|
|
221
|
+
expect(p.getCurrentThinking()).toBeNull();
|
|
222
|
+
expect(p.getFinalResult()).toBeNull();
|
|
223
|
+
expect(p.consumeFatalErrorMessage()).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { replaceBasePromptIdentity } from "../openclaw/worker";
|
|
3
|
+
|
|
4
|
+
const PI_OPENER =
|
|
5
|
+
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
6
|
+
|
|
7
|
+
describe("replaceBasePromptIdentity", () => {
|
|
8
|
+
test("replaces the pi-coding-agent opener with agent identity, preserving the rest", () => {
|
|
9
|
+
const base = `${PI_OPENER}\n\nAvailable tools:\n- read: Read file contents\n\nGuidelines:\n- Be concise`;
|
|
10
|
+
const identity = "You are a healthcare operations assistant.";
|
|
11
|
+
const out = replaceBasePromptIdentity(base, identity);
|
|
12
|
+
|
|
13
|
+
expect(out.startsWith(identity)).toBe(true);
|
|
14
|
+
expect(out).not.toContain("expert coding assistant");
|
|
15
|
+
// Preserved harness footer
|
|
16
|
+
expect(out).toContain("Available tools:");
|
|
17
|
+
expect(out).toContain("Guidelines:");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("falls back to prepending identity when upstream opener wording drifts", () => {
|
|
21
|
+
const base =
|
|
22
|
+
"You are some other intro that the upstream package switched to.\n\nAvailable tools:\n- read";
|
|
23
|
+
const identity = "You are a healthcare operations assistant.";
|
|
24
|
+
const out = replaceBasePromptIdentity(base, identity);
|
|
25
|
+
|
|
26
|
+
expect(out.startsWith(identity)).toBe(true);
|
|
27
|
+
// Original base prompt is still there (we didn't accidentally drop it)
|
|
28
|
+
expect(out).toContain("Available tools:");
|
|
29
|
+
expect(out).toContain("some other intro");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("multi-line identity is inserted as a single block", () => {
|
|
33
|
+
const base = `${PI_OPENER}\n\nAvailable tools:\n- read`;
|
|
34
|
+
const identity =
|
|
35
|
+
"You are a careops bot.\n\nYou speak only in plain English.";
|
|
36
|
+
const out = replaceBasePromptIdentity(base, identity);
|
|
37
|
+
|
|
38
|
+
expect(out.startsWith(identity)).toBe(true);
|
|
39
|
+
expect(out).toContain("Available tools:");
|
|
40
|
+
});
|
|
41
|
+
});
|