@lobu/worker 6.1.1 → 7.1.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.
Files changed (124) hide show
  1. package/dist/core/error-handler.d.ts +0 -4
  2. package/dist/core/error-handler.d.ts.map +1 -1
  3. package/dist/core/error-handler.js +4 -15
  4. package/dist/core/error-handler.js.map +1 -1
  5. package/dist/core/types.d.ts +1 -19
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/core/types.js +0 -4
  8. package/dist/core/types.js.map +1 -1
  9. package/dist/core/workspace.d.ts +2 -11
  10. package/dist/core/workspace.d.ts.map +1 -1
  11. package/dist/core/workspace.js +14 -36
  12. package/dist/core/workspace.js.map +1 -1
  13. package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
  14. package/dist/embedded/just-bash-bootstrap.js +60 -6
  15. package/dist/embedded/just-bash-bootstrap.js.map +1 -1
  16. package/dist/embedded/mcp-cli-commands.d.ts.map +1 -1
  17. package/dist/embedded/mcp-cli-commands.js +3 -38
  18. package/dist/embedded/mcp-cli-commands.js.map +1 -1
  19. package/dist/gateway/gateway-integration.js +4 -4
  20. package/dist/gateway/gateway-integration.js.map +1 -1
  21. package/dist/gateway/message-batcher.d.ts.map +1 -1
  22. package/dist/gateway/message-batcher.js +3 -5
  23. package/dist/gateway/message-batcher.js.map +1 -1
  24. package/dist/gateway/sse-client.d.ts +1 -0
  25. package/dist/gateway/sse-client.d.ts.map +1 -1
  26. package/dist/gateway/sse-client.js +52 -8
  27. package/dist/gateway/sse-client.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +7 -24
  30. package/dist/index.js.map +1 -1
  31. package/dist/instructions/builder.d.ts.map +1 -1
  32. package/dist/instructions/builder.js +2 -1
  33. package/dist/instructions/builder.js.map +1 -1
  34. package/dist/openclaw/plugin-loader.d.ts.map +1 -1
  35. package/dist/openclaw/plugin-loader.js +8 -19
  36. package/dist/openclaw/plugin-loader.js.map +1 -1
  37. package/dist/openclaw/processor.d.ts.map +1 -1
  38. package/dist/openclaw/processor.js +2 -0
  39. package/dist/openclaw/processor.js.map +1 -1
  40. package/dist/openclaw/sandbox-leak.d.ts.map +1 -1
  41. package/dist/openclaw/sandbox-leak.js +1 -6
  42. package/dist/openclaw/sandbox-leak.js.map +1 -1
  43. package/dist/openclaw/session-context.d.ts.map +1 -1
  44. package/dist/openclaw/session-context.js +3 -0
  45. package/dist/openclaw/session-context.js.map +1 -1
  46. package/dist/openclaw/tool-policy.d.ts.map +1 -1
  47. package/dist/openclaw/tool-policy.js +5 -11
  48. package/dist/openclaw/tool-policy.js.map +1 -1
  49. package/dist/openclaw/worker.d.ts +0 -1
  50. package/dist/openclaw/worker.d.ts.map +1 -1
  51. package/dist/openclaw/worker.js +19 -85
  52. package/dist/openclaw/worker.js.map +1 -1
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +3 -40
  55. package/dist/server.js.map +1 -1
  56. package/dist/shared/audio-provider-suggestions.d.ts.map +1 -1
  57. package/dist/shared/audio-provider-suggestions.js +4 -6
  58. package/dist/shared/audio-provider-suggestions.js.map +1 -1
  59. package/dist/shared/tool-implementations.d.ts.map +1 -1
  60. package/dist/shared/tool-implementations.js +99 -37
  61. package/dist/shared/tool-implementations.js.map +1 -1
  62. package/package.json +14 -4
  63. package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
  64. package/src/__tests__/custom-tools.test.ts +92 -0
  65. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
  66. package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
  67. package/src/__tests__/embedded-tools.test.ts +744 -0
  68. package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
  69. package/src/__tests__/exec-sandbox.test.ts +550 -0
  70. package/src/__tests__/generated-media.test.ts +142 -0
  71. package/src/__tests__/instructions.test.ts +60 -0
  72. package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
  73. package/src/__tests__/mcp-cli-commands.test.ts +383 -0
  74. package/src/__tests__/mcp-tool-call.test.ts +423 -0
  75. package/src/__tests__/memory-flush-harden.test.ts +367 -0
  76. package/src/__tests__/memory-flush-runtime.test.ts +138 -0
  77. package/src/__tests__/memory-flush.test.ts +64 -0
  78. package/src/__tests__/message-batcher.test.ts +247 -0
  79. package/src/__tests__/model-resolver-harden.test.ts +197 -0
  80. package/src/__tests__/model-resolver.test.ts +156 -0
  81. package/src/__tests__/processor-harden.test.ts +259 -0
  82. package/src/__tests__/processor.test.ts +225 -0
  83. package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
  84. package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
  85. package/src/__tests__/sandbox-leak.test.ts +167 -0
  86. package/src/__tests__/setup.ts +102 -0
  87. package/src/__tests__/sse-client-harden.test.ts +588 -0
  88. package/src/__tests__/sse-client.test.ts +90 -0
  89. package/src/__tests__/tool-implementations.test.ts +196 -0
  90. package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
  91. package/src/__tests__/tool-policy.test.ts +269 -0
  92. package/src/__tests__/worker.test.ts +89 -0
  93. package/src/core/error-handler.ts +47 -0
  94. package/src/core/project-scanner.ts +65 -0
  95. package/src/core/types.ts +94 -0
  96. package/src/core/workspace.ts +66 -0
  97. package/src/embedded/exec-sandbox.ts +372 -0
  98. package/src/embedded/just-bash-bootstrap.ts +575 -0
  99. package/src/embedded/mcp-cli-commands.ts +405 -0
  100. package/src/gateway/gateway-integration.ts +298 -0
  101. package/src/gateway/message-batcher.ts +123 -0
  102. package/src/gateway/sse-client.ts +988 -0
  103. package/src/gateway/types.ts +68 -0
  104. package/src/index.ts +123 -0
  105. package/src/instructions/builder.ts +44 -0
  106. package/src/instructions/providers.ts +27 -0
  107. package/src/modules/lifecycle.ts +92 -0
  108. package/src/openclaw/custom-tools.ts +315 -0
  109. package/src/openclaw/instructions.ts +36 -0
  110. package/src/openclaw/model-resolver.ts +150 -0
  111. package/src/openclaw/plugin-loader.ts +423 -0
  112. package/src/openclaw/processor.ts +199 -0
  113. package/src/openclaw/sandbox-leak.ts +100 -0
  114. package/src/openclaw/session-context.ts +323 -0
  115. package/src/openclaw/tool-policy.ts +241 -0
  116. package/src/openclaw/tools.ts +277 -0
  117. package/src/openclaw/worker.ts +1836 -0
  118. package/src/server.ts +330 -0
  119. package/src/shared/audio-provider-suggestions.ts +130 -0
  120. package/src/shared/processor-utils.ts +33 -0
  121. package/src/shared/provider-auth-hints.ts +68 -0
  122. package/src/shared/tool-display-config.ts +75 -0
  123. package/src/shared/tool-implementations.ts +981 -0
  124. package/src/shared/worker-env-keys.ts +8 -0
@@ -0,0 +1,259 @@
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 without throwing", () => {
49
+ const p = new OpenClawProgressProcessor();
50
+ const event = {
51
+ type: "message_update",
52
+ message: { role: "assistant" },
53
+ assistantMessageEvent: null,
54
+ };
55
+ let result: boolean | undefined;
56
+ expect(() => {
57
+ result = p.processEvent(event as any);
58
+ }).not.toThrow();
59
+ expect(result).toBe(false);
60
+ expect(p.getDelta()).toBeNull();
61
+ });
62
+
63
+ test("tool_execution_start with null args does not throw", () => {
64
+ const p = new OpenClawProgressProcessor();
65
+ expect(() =>
66
+ p.processEvent({
67
+ type: "tool_execution_start",
68
+ toolName: "Read",
69
+ args: null,
70
+ } as any)
71
+ ).not.toThrow();
72
+ });
73
+
74
+ test("tool_execution_start with string args does not throw", () => {
75
+ const p = new OpenClawProgressProcessor();
76
+ expect(() =>
77
+ p.processEvent({
78
+ type: "tool_execution_start",
79
+ toolName: "Read",
80
+ args: "unexpected-string",
81
+ } as any)
82
+ ).not.toThrow();
83
+ });
84
+
85
+ test("auto_compaction_end with neither aborted nor result returns true", () => {
86
+ const p = new OpenClawProgressProcessor();
87
+ // No aborted, no result — the current implementation still returns true
88
+ const result = p.processEvent({ type: "auto_compaction_end" } as any);
89
+ expect(result).toBe(true);
90
+ });
91
+
92
+ test("auto_retry_end with success=true and no finalError returns false", () => {
93
+ const p = new OpenClawProgressProcessor();
94
+ const result = p.processEvent({
95
+ type: "auto_retry_end",
96
+ success: true,
97
+ finalError: undefined,
98
+ } as any);
99
+ expect(result).toBe(false);
100
+ });
101
+ });
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // message_end content extraction edge cases
105
+ // ---------------------------------------------------------------------------
106
+
107
+ describe("message_end content extraction", () => {
108
+ test("empty content array produces no output", () => {
109
+ const p = new OpenClawProgressProcessor();
110
+ const result = p.processEvent(makeMessageEnd({ content: [] }));
111
+ expect(result).toBe(false);
112
+ expect(p.getDelta()).toBeNull();
113
+ });
114
+
115
+ test("whitespace-only text block produces no output", () => {
116
+ const p = new OpenClawProgressProcessor();
117
+ const result = p.processEvent(
118
+ makeMessageEnd({ content: [{ type: "text", text: " \n\t " }] })
119
+ );
120
+ expect(result).toBe(false);
121
+ expect(p.getDelta()).toBeNull();
122
+ });
123
+
124
+ test("non-text blocks are skipped", () => {
125
+ const p = new OpenClawProgressProcessor();
126
+ const result = p.processEvent(
127
+ makeMessageEnd({ content: [{ type: "tool_use", id: "x" }] })
128
+ );
129
+ expect(result).toBe(false);
130
+ });
131
+
132
+ test("multiple text blocks concatenated", () => {
133
+ const p = new OpenClawProgressProcessor();
134
+ p.processEvent(
135
+ makeMessageEnd({
136
+ content: [
137
+ { type: "text", text: "Hello" },
138
+ { type: "text", text: " World" },
139
+ ],
140
+ })
141
+ );
142
+ expect(p.getDelta()).toContain("Hello World");
143
+ });
144
+
145
+ test("message_end with error does not append content text", () => {
146
+ const p = new OpenClawProgressProcessor();
147
+ p.processEvent(
148
+ makeMessageEnd({
149
+ stopReason: "error",
150
+ errorMessage: "Boom",
151
+ content: [{ type: "text", text: "Should not appear" }],
152
+ })
153
+ );
154
+ expect(p.getDelta()).toBeNull();
155
+ expect(p.consumeFatalErrorMessage()).toBe("Boom");
156
+ });
157
+
158
+ test("message_end after streaming skips re-extraction", () => {
159
+ const p = new OpenClawProgressProcessor();
160
+ p.processEvent(makeTextDelta("streamed text"));
161
+ p.getDelta();
162
+
163
+ // Now simulate message_end with content
164
+ const result = p.processEvent(
165
+ makeMessageEnd({ content: [{ type: "text", text: "duplicate" }] })
166
+ );
167
+ expect(result).toBe(false);
168
+ expect(p.getDelta()).toBeNull();
169
+ });
170
+
171
+ test("after reset(), message_end re-extracts text even if streaming happened before", () => {
172
+ const p = new OpenClawProgressProcessor();
173
+ p.processEvent(makeTextDelta("before reset"));
174
+ p.getDelta();
175
+
176
+ p.reset();
177
+
178
+ // Now message_end should be able to extract (hasStreamedText reset)
179
+ const result = p.processEvent(
180
+ makeMessageEnd({ content: [{ type: "text", text: "after reset" }] })
181
+ );
182
+ expect(result).toBe(true);
183
+ expect(p.getDelta()).toContain("after reset");
184
+ });
185
+ });
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // getDelta monotonicity and snapshot
189
+ // ---------------------------------------------------------------------------
190
+
191
+ describe("getDelta and getOutputSnapshot", () => {
192
+ test("multiple incremental deltas return cumulative suffix each time", () => {
193
+ const p = new OpenClawProgressProcessor();
194
+ p.processEvent(makeTextDelta("A"));
195
+ expect(p.getDelta()).toBe("A");
196
+
197
+ p.processEvent(makeTextDelta("B"));
198
+ expect(p.getDelta()).toBe("B");
199
+
200
+ p.processEvent(makeTextDelta("C"));
201
+ expect(p.getDelta()).toBe("C");
202
+ });
203
+
204
+ test("getOutputSnapshot returns full accumulated output", () => {
205
+ const p = new OpenClawProgressProcessor();
206
+ p.processEvent(makeTextDelta("part1"));
207
+ p.getDelta(); // consume
208
+
209
+ p.processEvent(makeTextDelta(" part2"));
210
+ p.getDelta(); // consume
211
+
212
+ // Snapshot reflects everything even after getDelta consumed it
213
+ expect(p.getOutputSnapshot()).toBe("part1 part2");
214
+ });
215
+
216
+ test("getDelta returns full content when it diverges from lastSentContent", () => {
217
+ const p = new OpenClawProgressProcessor();
218
+ p.processEvent(makeTextDelta("original"));
219
+ p.getDelta(); // lastSentContent = "original"
220
+
221
+ // Simulate a reset + new content that doesn't start with old prefix
222
+ p.reset();
223
+ p.processEvent(makeTextDelta("completely different"));
224
+ // Full content is returned since it doesn't start with lastSentContent (empty after reset)
225
+ expect(p.getDelta()).toBe("completely different");
226
+ });
227
+ });
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Thinking accumulation across verbose/non-verbose
231
+ // ---------------------------------------------------------------------------
232
+
233
+ describe("thinking accumulation", () => {
234
+ test("thinking deltas accumulate across multiple events", () => {
235
+ const p = new OpenClawProgressProcessor();
236
+ p.processEvent({
237
+ type: "message_update",
238
+ message: { role: "assistant" },
239
+ assistantMessageEvent: { type: "thinking_delta", delta: "step one " },
240
+ } as any);
241
+ p.processEvent({
242
+ type: "message_update",
243
+ message: { role: "assistant" },
244
+ assistantMessageEvent: { type: "thinking_delta", delta: "step two" },
245
+ } as any);
246
+ expect(p.getCurrentThinking()).toBe("step one step two");
247
+ });
248
+
249
+ test("reset() clears accumulated thinking", () => {
250
+ const p = new OpenClawProgressProcessor();
251
+ p.processEvent({
252
+ type: "message_update",
253
+ message: { role: "assistant" },
254
+ assistantMessageEvent: { type: "thinking_delta", delta: "thoughts" },
255
+ } as any);
256
+ p.reset();
257
+ expect(p.getCurrentThinking()).toBeNull();
258
+ });
259
+ });
@@ -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
+ });