@lobu/worker 6.0.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.
Files changed (104) hide show
  1. package/dist/embedded/exec-sandbox.d.ts +2 -2
  2. package/dist/embedded/exec-sandbox.js +7 -7
  3. package/dist/embedded/exec-sandbox.js.map +1 -1
  4. package/dist/embedded/just-bash-bootstrap.d.ts +2 -2
  5. package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
  6. package/dist/embedded/just-bash-bootstrap.js +30 -6
  7. package/dist/embedded/just-bash-bootstrap.js.map +1 -1
  8. package/dist/embedded/mcp-cli-commands.d.ts +5 -5
  9. package/dist/gateway/gateway-integration.js +4 -4
  10. package/dist/gateway/gateway-integration.js.map +1 -1
  11. package/dist/gateway/message-batcher.d.ts.map +1 -1
  12. package/dist/gateway/message-batcher.js +3 -5
  13. package/dist/gateway/message-batcher.js.map +1 -1
  14. package/dist/gateway/sse-client.d.ts +1 -0
  15. package/dist/gateway/sse-client.d.ts.map +1 -1
  16. package/dist/gateway/sse-client.js +13 -8
  17. package/dist/gateway/sse-client.js.map +1 -1
  18. package/dist/gateway/types.d.ts +1 -1
  19. package/dist/gateway/types.d.ts.map +1 -1
  20. package/dist/instructions/builder.d.ts +4 -0
  21. package/dist/instructions/builder.d.ts.map +1 -1
  22. package/dist/instructions/builder.js +8 -11
  23. package/dist/instructions/builder.js.map +1 -1
  24. package/dist/instructions/providers.d.ts +5 -5
  25. package/dist/instructions/providers.d.ts.map +1 -1
  26. package/dist/instructions/providers.js +3 -2
  27. package/dist/instructions/providers.js.map +1 -1
  28. package/dist/openclaw/custom-tools.d.ts +1 -1
  29. package/dist/openclaw/custom-tools.js +1 -1
  30. package/dist/openclaw/instructions.d.ts +9 -9
  31. package/dist/openclaw/instructions.d.ts.map +1 -1
  32. package/dist/openclaw/instructions.js +4 -4
  33. package/dist/openclaw/instructions.js.map +1 -1
  34. package/dist/openclaw/tools.js.map +1 -1
  35. package/dist/openclaw/worker.d.ts +0 -1
  36. package/dist/openclaw/worker.d.ts.map +1 -1
  37. package/dist/openclaw/worker.js +18 -75
  38. package/dist/openclaw/worker.js.map +1 -1
  39. package/dist/shared/tool-implementations.d.ts.map +1 -1
  40. package/dist/shared/tool-implementations.js +37 -13
  41. package/dist/shared/tool-implementations.js.map +1 -1
  42. package/package.json +14 -4
  43. package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
  44. package/src/__tests__/custom-tools.test.ts +92 -0
  45. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
  46. package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
  47. package/src/__tests__/embedded-tools.test.ts +744 -0
  48. package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
  49. package/src/__tests__/exec-sandbox.test.ts +550 -0
  50. package/src/__tests__/generated-media.test.ts +142 -0
  51. package/src/__tests__/instructions.test.ts +60 -0
  52. package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
  53. package/src/__tests__/mcp-cli-commands.test.ts +383 -0
  54. package/src/__tests__/mcp-tool-call.test.ts +423 -0
  55. package/src/__tests__/memory-flush-harden.test.ts +367 -0
  56. package/src/__tests__/memory-flush-runtime.test.ts +138 -0
  57. package/src/__tests__/memory-flush.test.ts +64 -0
  58. package/src/__tests__/message-batcher.test.ts +247 -0
  59. package/src/__tests__/model-resolver-harden.test.ts +197 -0
  60. package/src/__tests__/model-resolver.test.ts +156 -0
  61. package/src/__tests__/processor-harden.test.ts +269 -0
  62. package/src/__tests__/processor.test.ts +225 -0
  63. package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
  64. package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
  65. package/src/__tests__/sandbox-leak.test.ts +167 -0
  66. package/src/__tests__/setup.ts +102 -0
  67. package/src/__tests__/sse-client-harden.test.ts +588 -0
  68. package/src/__tests__/sse-client.test.ts +90 -0
  69. package/src/__tests__/tool-implementations.test.ts +196 -0
  70. package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
  71. package/src/__tests__/tool-policy.test.ts +269 -0
  72. package/src/__tests__/worker.test.ts +89 -0
  73. package/src/core/error-handler.ts +62 -0
  74. package/src/core/project-scanner.ts +65 -0
  75. package/src/core/types.ts +128 -0
  76. package/src/core/workspace.ts +89 -0
  77. package/src/embedded/exec-sandbox.ts +372 -0
  78. package/src/embedded/just-bash-bootstrap.ts +543 -0
  79. package/src/embedded/mcp-cli-commands.ts +402 -0
  80. package/src/gateway/gateway-integration.ts +298 -0
  81. package/src/gateway/message-batcher.ts +123 -0
  82. package/src/gateway/sse-client.ts +951 -0
  83. package/src/gateway/types.ts +68 -0
  84. package/src/index.ts +141 -0
  85. package/src/instructions/builder.ts +45 -0
  86. package/src/instructions/providers.ts +27 -0
  87. package/src/modules/lifecycle.ts +92 -0
  88. package/src/openclaw/custom-tools.ts +315 -0
  89. package/src/openclaw/instructions.ts +36 -0
  90. package/src/openclaw/model-resolver.ts +150 -0
  91. package/src/openclaw/plugin-loader.ts +427 -0
  92. package/src/openclaw/processor.ts +198 -0
  93. package/src/openclaw/sandbox-leak.ts +105 -0
  94. package/src/openclaw/session-context.ts +320 -0
  95. package/src/openclaw/tool-policy.ts +248 -0
  96. package/src/openclaw/tools.ts +277 -0
  97. package/src/openclaw/worker.ts +1847 -0
  98. package/src/server.ts +334 -0
  99. package/src/shared/audio-provider-suggestions.ts +132 -0
  100. package/src/shared/processor-utils.ts +33 -0
  101. package/src/shared/provider-auth-hints.ts +68 -0
  102. package/src/shared/tool-display-config.ts +75 -0
  103. package/src/shared/tool-implementations.ts +940 -0
  104. package/src/shared/worker-env-keys.ts +8 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Hardening tests for checkSandboxLeak — edge cases not covered by sandbox-leak.test.ts.
3
+ *
4
+ * Covers:
5
+ * - Multiple leak patterns in one message (all redacted, single note appended)
6
+ * - Real-secret-shaped strings that are NOT workspace paths (no false positive)
7
+ * - Boundary: path with no extension is NOT flagged by delivery-phrase regex
8
+ * - sandbox:// URL inside a code block (still flagged — delivery claim)
9
+ * - HTML src attribute without a file extension (still a workspace link → flagged)
10
+ * - The "exported to" / "written to" / "generated at" delivery phrase variants
11
+ * - Regex lastIndex stability under rapid repeated calls (stress)
12
+ * - Large input with no leaks: does not incorrectly set leaked=true
13
+ */
14
+
15
+ import { describe, expect, test } from "bun:test";
16
+ import { checkSandboxLeak } from "../openclaw/sandbox-leak";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Multiple patterns in one message
20
+ // ---------------------------------------------------------------------------
21
+
22
+ describe("checkSandboxLeak — multiple leak patterns", () => {
23
+ test("redacts all offending patterns and appends a single note", () => {
24
+ const text = [
25
+ "Your files: sandbox:/report.pdf",
26
+ "[csv](/app/workspaces/org/123/data.csv)",
27
+ '<img src="/workspace/chart.png" />',
28
+ ].join("\n");
29
+
30
+ const res = checkSandboxLeak(text, false);
31
+ expect(res.leaked).toBe(true);
32
+
33
+ // sandbox URL replaced
34
+ expect(res.redactedText).toContain("[local file, not uploaded]");
35
+ // markdown link target replaced
36
+ expect(res.redactedText).toContain("](about:blank)");
37
+ // HTML src replaced
38
+ expect(res.redactedText).toContain('src="about:blank"');
39
+
40
+ // Only one appended note block
41
+ const noteCount = (
42
+ res.redactedText.match(/_Note: I referenced a local file/g) ?? []
43
+ ).length;
44
+ expect(noteCount).toBe(1);
45
+ });
46
+ });
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // False-positive guard: real-secret-shaped text not in a workspace path
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe("checkSandboxLeak — false-positive guards", () => {
53
+ test("ANTHROPIC_API_KEY in plain prose is not flagged", () => {
54
+ const text =
55
+ "Never log ANTHROPIC_API_KEY or sk-ant-api03-xxxx values in responses.";
56
+ const res = checkSandboxLeak(text, false);
57
+ expect(res.leaked).toBe(false);
58
+ expect(res.redactedText).toBe(text);
59
+ });
60
+
61
+ test("lobu_secret placeholder string in prose is not flagged", () => {
62
+ const text =
63
+ "The API key is lobu_secret_abc123 — a proxy placeholder, not the real key.";
64
+ const res = checkSandboxLeak(text, false);
65
+ expect(res.leaked).toBe(false);
66
+ expect(res.redactedText).toBe(text);
67
+ });
68
+
69
+ test("HTTP URL starting with https is not flagged", () => {
70
+ const text = "Download from https://example.com/report.pdf.";
71
+ const res = checkSandboxLeak(text, false);
72
+ expect(res.leaked).toBe(false);
73
+ });
74
+
75
+ test("/tmp path without workspace prefix is not flagged", () => {
76
+ const text = "Temp file at /tmp/output.csv is ephemeral.";
77
+ const res = checkSandboxLeak(text, false);
78
+ expect(res.leaked).toBe(false);
79
+ });
80
+ });
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Delivery phrase variants
84
+ // ---------------------------------------------------------------------------
85
+
86
+ describe("checkSandboxLeak — delivery phrase variants", () => {
87
+ test("'exported to' triggers detection", () => {
88
+ const text = "exported to /app/workspaces/org/123/report.csv";
89
+ const res = checkSandboxLeak(text, false);
90
+ expect(res.leaked).toBe(true);
91
+ });
92
+
93
+ test("'written to' triggers detection", () => {
94
+ const text = "written to /workspace/output/final.txt";
95
+ const res = checkSandboxLeak(text, false);
96
+ expect(res.leaked).toBe(true);
97
+ });
98
+
99
+ test("'generated at' triggers detection", () => {
100
+ const text = "generated at /workspace/artifacts/report.pdf";
101
+ const res = checkSandboxLeak(text, false);
102
+ expect(res.leaked).toBe(true);
103
+ });
104
+
105
+ test("'created at' triggers detection", () => {
106
+ const text = "The summary was created at /workspace/summary.md";
107
+ const res = checkSandboxLeak(text, false);
108
+ expect(res.leaked).toBe(true);
109
+ });
110
+
111
+ test("'stored at' triggers detection", () => {
112
+ const text = "The data is stored at /app/workspaces/org/run/data.json";
113
+ const res = checkSandboxLeak(text, false);
114
+ expect(res.leaked).toBe(true);
115
+ });
116
+
117
+ test("delivery phrase with a very long extension is still caught (up to 10 chars)", () => {
118
+ const text = "located at /workspace/compressed.tar.gz";
119
+ // tar.gz — the regex matches \.\w{1,10} so 'gz' qualifies
120
+ const res = checkSandboxLeak(text, false);
121
+ expect(res.leaked).toBe(true);
122
+ });
123
+ });
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // sandbox:// inside prose / code blocks
127
+ // ---------------------------------------------------------------------------
128
+
129
+ describe("checkSandboxLeak — sandbox:// variants", () => {
130
+ test("sandbox:// inside backtick code span is still flagged", () => {
131
+ const text = "Run `curl sandbox://output/data.csv` to download.";
132
+ const res = checkSandboxLeak(text, false);
133
+ expect(res.leaked).toBe(true);
134
+ });
135
+
136
+ test("sandbox://workspace/path with query string is flagged", () => {
137
+ // The regex stops at whitespace/special chars — but ? is included until space
138
+ const text = "Here: sandbox://workspace/report.pdf";
139
+ const res = checkSandboxLeak(text, false);
140
+ expect(res.leaked).toBe(true);
141
+ });
142
+ });
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // HTML attribute variants
146
+ // ---------------------------------------------------------------------------
147
+
148
+ describe("checkSandboxLeak — HTML attributes", () => {
149
+ test("single-quote HTML href is flagged", () => {
150
+ const text = "<a href='/app/workspaces/org/report.pdf'>click</a>";
151
+ const res = checkSandboxLeak(text, false);
152
+ expect(res.leaked).toBe(true);
153
+ expect(res.redactedText).toContain('href="about:blank"');
154
+ });
155
+
156
+ test("HTML src without extension is still flagged (binary without ext)", () => {
157
+ const text = '<img src="/workspace/artifact" />';
158
+ // No extension — the LOCAL_HREF_RE doesn't require an extension
159
+ const res = checkSandboxLeak(text, false);
160
+ expect(res.leaked).toBe(true);
161
+ });
162
+ });
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Regex stability under rapid repeated calls (lastIndex leak check)
166
+ // ---------------------------------------------------------------------------
167
+
168
+ describe("checkSandboxLeak — regex stability under rapid invocations", () => {
169
+ test("alternating positive/negative checks remain accurate across 20 calls", () => {
170
+ const good = "Workspace information: `/workspace/dir`";
171
+ const bad = "sandbox:/workspace/file.txt";
172
+
173
+ for (let i = 0; i < 20; i++) {
174
+ const g = checkSandboxLeak(good, false);
175
+ const b = checkSandboxLeak(bad, false);
176
+ expect(g.leaked).toBe(false);
177
+ expect(b.leaked).toBe(true);
178
+ }
179
+ });
180
+
181
+ test("100 consecutive 'no leak' calls return consistent false", () => {
182
+ const text = "The workspace directory is at `/app/workspaces/org/run`.";
183
+ for (let i = 0; i < 100; i++) {
184
+ expect(checkSandboxLeak(text, false).leaked).toBe(false);
185
+ }
186
+ });
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Large input with no leaks
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe("checkSandboxLeak — large clean input", () => {
194
+ test("large prose with no workspace paths is not flagged", () => {
195
+ const para = "This is a paragraph about agent workflows. ".repeat(200);
196
+ const res = checkSandboxLeak(para, false);
197
+ expect(res.leaked).toBe(false);
198
+ expect(res.redactedText).toBe(para);
199
+ });
200
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Unit tests for the sandbox-leak detector. These cover the false-positive
3
+ * cases that broke the Slack "probe" response (descriptive path mentions
4
+ * got nuked) plus the true-positive cases we still need to catch.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { checkSandboxLeak } from "../openclaw/sandbox-leak";
9
+
10
+ describe("checkSandboxLeak", () => {
11
+ test("passes through empty input", () => {
12
+ const res = checkSandboxLeak("", false);
13
+ expect(res.leaked).toBe(false);
14
+ expect(res.redactedText).toBe("");
15
+ });
16
+
17
+ test("suppresses check when UploadUserFile event was seen", () => {
18
+ const text =
19
+ "Here is your report. Also my notes live at /app/workspaces/foo/bar.md.";
20
+ const res = checkSandboxLeak(text, true);
21
+ expect(res.leaked).toBe(false);
22
+ expect(res.redactedText).toBe(text);
23
+ });
24
+
25
+ test("allows descriptive probe response mentioning workspace path", () => {
26
+ const text = [
27
+ "## Workspace Probe Results",
28
+ "",
29
+ "**Workspace Location:** `/app/workspaces/careops/C09EH3ASNQ1`",
30
+ "",
31
+ "**Directory Structure:**",
32
+ "- `.openclaw/` - Configuration files",
33
+ "- `input/` - Empty",
34
+ ].join("\n");
35
+ const res = checkSandboxLeak(text, false);
36
+ expect(res.leaked).toBe(false);
37
+ expect(res.redactedText).toBe(text);
38
+ });
39
+
40
+ test("allows /workspace/ path in plain prose", () => {
41
+ const text =
42
+ "I inspected /workspace/careops and it contains three directories.";
43
+ const res = checkSandboxLeak(text, false);
44
+ expect(res.leaked).toBe(false);
45
+ });
46
+
47
+ test("flags sandbox:// URL as delivery claim", () => {
48
+ const text = "Here is your file: sandbox:/output/report.pdf";
49
+ const res = checkSandboxLeak(text, false);
50
+ expect(res.leaked).toBe(true);
51
+ expect(res.redactedText).not.toContain("sandbox:/output/report.pdf");
52
+ expect(res.redactedText).toContain("[local file, not uploaded]");
53
+ expect(res.redactedText).toContain("did not actually upload");
54
+ });
55
+
56
+ test("flags sandbox:// URL with double-slash form", () => {
57
+ const text = "Download: sandbox://workspace/x.csv";
58
+ const res = checkSandboxLeak(text, false);
59
+ expect(res.leaked).toBe(true);
60
+ expect(res.redactedText).not.toContain("sandbox://workspace/x.csv");
61
+ });
62
+
63
+ test("flags markdown link to /app/workspaces/ as delivery claim", () => {
64
+ const text =
65
+ "Your report is ready: [report.pdf](/app/workspaces/foo/report.pdf)";
66
+ const res = checkSandboxLeak(text, false);
67
+ expect(res.leaked).toBe(true);
68
+ expect(res.redactedText).not.toContain("/app/workspaces/foo/report.pdf");
69
+ expect(res.redactedText).toContain("](about:blank)");
70
+ expect(res.redactedText).toContain("did not actually upload");
71
+ });
72
+
73
+ test("flags markdown link with file:// scheme", () => {
74
+ const text = "Download [here](file:///workspace/out.csv).";
75
+ const res = checkSandboxLeak(text, false);
76
+ expect(res.leaked).toBe(true);
77
+ expect(res.redactedText).not.toContain("file:///workspace/out.csv");
78
+ });
79
+
80
+ test("flags HTML href pointing at workspace path", () => {
81
+ const text = '<a href="/app/workspaces/foo/bar.pdf">click</a>';
82
+ const res = checkSandboxLeak(text, false);
83
+ expect(res.leaked).toBe(true);
84
+ expect(res.redactedText).toContain('href="about:blank"');
85
+ expect(res.redactedText).not.toContain("/app/workspaces/foo/bar.pdf");
86
+ });
87
+
88
+ test("flags HTML src preserving attribute name", () => {
89
+ const text = '<img src="/workspace/chart.png" />';
90
+ const res = checkSandboxLeak(text, false);
91
+ expect(res.leaked).toBe(true);
92
+ expect(res.redactedText).toContain('src="about:blank"');
93
+ expect(res.redactedText).not.toContain("/workspace/chart.png");
94
+ });
95
+
96
+ test("preserves surrounding prose when redacting", () => {
97
+ const text =
98
+ "Intro paragraph.\n\n[report.pdf](/app/workspaces/x/report.pdf)\n\nClosing remarks.";
99
+ const res = checkSandboxLeak(text, false);
100
+ expect(res.leaked).toBe(true);
101
+ expect(res.redactedText).toContain("Intro paragraph.");
102
+ expect(res.redactedText).toContain("Closing remarks.");
103
+ });
104
+
105
+ test("does not flag bare workspace path in backticks", () => {
106
+ // This is the probe-style case that the broad regex was false-positive on.
107
+ const text = "The workspace is at `/app/workspaces/careops/foo`.";
108
+ const res = checkSandboxLeak(text, false);
109
+ expect(res.leaked).toBe(false);
110
+ expect(res.redactedText).toBe(text);
111
+ });
112
+
113
+ test("does not flag non-workspace markdown links", () => {
114
+ const text = "See [docs](https://example.com/path).";
115
+ const res = checkSandboxLeak(text, false);
116
+ expect(res.leaked).toBe(false);
117
+ });
118
+
119
+ // --- delivery-phrase detection ---
120
+
121
+ test("flags 'located at' with workspace file path", () => {
122
+ const text =
123
+ "The file is located at: /app/workspaces/careops/123/input/sample_patient.json";
124
+ const res = checkSandboxLeak(text, false);
125
+ expect(res.leaked).toBe(true);
126
+ expect(res.redactedText).not.toContain("/app/workspaces/careops");
127
+ expect(res.redactedText).toContain("not uploaded");
128
+ });
129
+
130
+ test("flags 'saved to' with backticked workspace path", () => {
131
+ const text = "Report saved to `/workspace/output/report.pdf` successfully.";
132
+ const res = checkSandboxLeak(text, false);
133
+ expect(res.leaked).toBe(true);
134
+ expect(res.redactedText).not.toContain("/workspace/output/report.pdf");
135
+ });
136
+
137
+ test("does not flag delivery phrase with directory (no extension)", () => {
138
+ const text = "Files are stored in /app/workspaces/careops/123/input";
139
+ const res = checkSandboxLeak(text, false);
140
+ expect(res.leaked).toBe(false);
141
+ });
142
+
143
+ test("does not flag delivery phrase about non-workspace paths", () => {
144
+ const text = "The file is located at: /home/user/documents/report.pdf";
145
+ const res = checkSandboxLeak(text, false);
146
+ expect(res.leaked).toBe(false);
147
+ });
148
+
149
+ test("suppresses delivery-phrase check when UploadUserFile was used", () => {
150
+ const text =
151
+ "I saved the file to `/workspace/output/data.csv` and uploaded it.";
152
+ const res = checkSandboxLeak(text, true);
153
+ expect(res.leaked).toBe(false);
154
+ expect(res.redactedText).toBe(text);
155
+ });
156
+
157
+ test("is stable across repeated invocations (no lastIndex leakage)", () => {
158
+ const bad = "Here: sandbox:/a.pdf";
159
+ const good = "Workspace at /app/workspaces/careops.";
160
+ const deliveryBad = "File saved to /app/workspaces/foo/report.pdf for you.";
161
+ expect(checkSandboxLeak(bad, false).leaked).toBe(true);
162
+ expect(checkSandboxLeak(good, false).leaked).toBe(false);
163
+ expect(checkSandboxLeak(deliveryBad, false).leaked).toBe(true);
164
+ expect(checkSandboxLeak(bad, false).leaked).toBe(true);
165
+ expect(checkSandboxLeak(good, false).leaked).toBe(false);
166
+ });
167
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Test setup and configuration for worker tests.
3
+ *
4
+ * Shared mocks live in @lobu/core fixtures.
5
+ * This file re-exports them and adds worker-specific helpers.
6
+ */
7
+
8
+ import { afterAll, beforeAll } from "bun:test";
9
+ import {
10
+ createWorkerConfig,
11
+ mockFetch as sharedMockFetch,
12
+ } from "@lobu/core/testing";
13
+
14
+ export const mockWorkerConfig = createWorkerConfig();
15
+
16
+ // Mock environment variables for testing
17
+ const mockEnvVars: Record<string, string> = {
18
+ DISPATCHER_URL: "https://test-dispatcher.example.com",
19
+ WORKER_TOKEN: "test-worker-token-123",
20
+ CONVERSATION_ID: "1234567890.123456",
21
+ WORKER_SESSION_KEY: "test-session-key",
22
+ WORKER_USER_ID: "U1234567890",
23
+ WORKER_CHANNEL_ID: "C1234567890",
24
+ WORKER_USER_PROMPT: Buffer.from("Test user prompt").toString("base64"),
25
+ WORKER_RESPONSE_CHANNEL: "C1234567890",
26
+ WORKER_RESPONSE_TS: "1234567890.123457",
27
+ WORKER_CLAUDE_OPTIONS: JSON.stringify({
28
+ model: "claude-sonnet-4-20250514",
29
+ max_tokens: 8192,
30
+ }),
31
+ WORKER_TEAM_ID: "T1234567890",
32
+ WORKER_WORKSPACE_BASE_DIRECTORY: "/tmp/test-workspace",
33
+ };
34
+
35
+ export class TestHelpers {
36
+ static mockFetch(responses?: Record<string, any>): () => void {
37
+ return sharedMockFetch(responses);
38
+ }
39
+
40
+ static createMockProgressUpdate(
41
+ type: "output" | "completion" | "error",
42
+ data: any
43
+ ) {
44
+ return { type, data, timestamp: Date.now() };
45
+ }
46
+
47
+ static async delay(ms: number): Promise<void> {
48
+ return new Promise((resolve) => setTimeout(resolve, ms));
49
+ }
50
+
51
+ static mockEventSource() {
52
+ class MockEventSource {
53
+ url: string;
54
+ readyState = 1;
55
+ onopen: ((event: Event) => void) | null = null;
56
+ onmessage: ((event: MessageEvent) => void) | null = null;
57
+ onerror: ((event: Event) => void) | null = null;
58
+
59
+ constructor(url: string) {
60
+ this.url = url;
61
+ setTimeout(() => {
62
+ if (this.onopen) this.onopen(new Event("open"));
63
+ }, 10);
64
+ }
65
+
66
+ close() {
67
+ this.readyState = 2;
68
+ }
69
+
70
+ simulateMessage(data: any) {
71
+ if (this.onmessage) {
72
+ this.onmessage(
73
+ new MessageEvent("message", { data: JSON.stringify(data) })
74
+ );
75
+ }
76
+ }
77
+
78
+ simulateError() {
79
+ if (this.onerror) this.onerror(new Event("error"));
80
+ }
81
+ }
82
+
83
+ const originalEventSource = globalThis.EventSource;
84
+ globalThis.EventSource = MockEventSource as any;
85
+ return () => {
86
+ globalThis.EventSource = originalEventSource;
87
+ };
88
+ }
89
+ }
90
+
91
+ // Global test setup
92
+ beforeAll(() => {
93
+ for (const [key, value] of Object.entries(mockEnvVars)) {
94
+ process.env[key] = value;
95
+ }
96
+ });
97
+
98
+ afterAll(() => {
99
+ for (const key of Object.keys(mockEnvVars)) {
100
+ delete process.env[key];
101
+ }
102
+ });