@poncho-ai/harness 0.50.3 → 0.50.5

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.
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { createIsolateRuntime } from "../src/isolate/runtime.js";
3
+ import { buildPolyfillPreamble } from "../src/isolate/polyfills.js";
3
4
  import type { IsolateBinding } from "../src/config.js";
4
5
 
5
6
  const DEFAULT_CONFIG = { memoryLimit: 64, timeout: 5000, outputLimit: 65536 };
7
+ const POLYFILLS = buildPolyfillPreamble(false);
6
8
 
7
9
  describe("IsolateRuntime", () => {
8
10
  it("executes basic JavaScript and returns a result", async () => {
@@ -136,6 +138,79 @@ describe("IsolateRuntime", () => {
136
138
  });
137
139
  });
138
140
 
141
+ describe("IsolateRuntime timers + wall-clock", () => {
142
+ it("resolves a non-zero setTimeout sleep instead of hanging", async () => {
143
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
144
+ const res = await runtime.execute(
145
+ `await new Promise(r => setTimeout(r, 50)); return "slept";`,
146
+ {},
147
+ null,
148
+ undefined,
149
+ POLYFILLS,
150
+ );
151
+
152
+ expect(res.error).toBeUndefined();
153
+ expect(res.result).toBe("slept");
154
+ });
155
+
156
+ it("runs awaited timers in delay order against the virtual clock", async () => {
157
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
158
+ const res = await runtime.execute(
159
+ `const order = [];
160
+ async function at(ms, label) {
161
+ await new Promise(r => setTimeout(r, ms));
162
+ order.push(label);
163
+ }
164
+ await Promise.all([at(100, "a"), at(10, "b"), at(50, "c")]);
165
+ return order;`,
166
+ {},
167
+ null,
168
+ undefined,
169
+ POLYFILLS,
170
+ );
171
+
172
+ expect(res.error).toBeUndefined();
173
+ expect(res.result).toEqual(["b", "c", "a"]);
174
+ });
175
+
176
+ it("supports setInterval + clearInterval", async () => {
177
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
178
+ const res = await runtime.execute(
179
+ `let n = 0;
180
+ await new Promise(resolve => {
181
+ const id = setInterval(() => {
182
+ n += 1;
183
+ if (n >= 3) { clearInterval(id); resolve(); }
184
+ }, 10);
185
+ });
186
+ return n;`,
187
+ {},
188
+ null,
189
+ undefined,
190
+ POLYFILLS,
191
+ );
192
+
193
+ expect(res.error).toBeUndefined();
194
+ expect(res.result).toBe(3);
195
+ });
196
+
197
+ it("times out a never-resolving promise via the wall-clock guard", async () => {
198
+ const runtime = createIsolateRuntime({ ...DEFAULT_CONFIG, timeout: 200 });
199
+ const start = performance.now();
200
+ const res = await runtime.execute(
201
+ `await new Promise(() => {}); return "never";`,
202
+ {},
203
+ null,
204
+ );
205
+
206
+ expect(res.error).toBeDefined();
207
+ expect(res.error!.message).toMatch(/timed out/i);
208
+ expect(res.error!.name).toBe("TimeoutError");
209
+ // Bounded by the wall clock, not hanging forever.
210
+ expect(performance.now() - start).toBeLessThan(2000);
211
+ });
212
+ });
213
+
139
214
  describe("IsolateRuntime bindings", () => {
140
215
  it("calls async bindings and returns results", async () => {
141
216
  const runtime = createIsolateRuntime(DEFAULT_CONFIG);
@@ -8,6 +8,9 @@ import {
8
8
  createTurnDraftState,
9
9
  recordStandardTurnEvent,
10
10
  executeConversationTurn,
11
+ lastAssistantText,
12
+ realResponseText,
13
+ abnormalEndResponse,
11
14
  } from "../src/orchestrator/index.js";
12
15
  import type { Conversation } from "../src/state.js";
13
16
 
@@ -174,3 +177,112 @@ describe("orchestrator helpers", () => {
174
177
  expect(seenTypes).toEqual(["run:started", "tool:started", "model:chunk", "run:completed"]);
175
178
  });
176
179
  });
180
+
181
+ describe("lastAssistantText (subagent result extraction)", () => {
182
+ it("returns a plain-string assistant message", () => {
183
+ const messages: Message[] = [
184
+ { role: "user", content: "find me 3 creators" },
185
+ { role: "assistant", content: "Here are 3 creators: ..." },
186
+ ];
187
+ expect(lastAssistantText(messages)).toBe("Here are 3 creators: ...");
188
+ });
189
+
190
+ it("unwraps the {text,tool_calls} envelope to its text", () => {
191
+ // How the run loop serializes an assistant turn that also called tools.
192
+ const envelope = JSON.stringify({
193
+ text: "Searching for candidates now.",
194
+ tool_calls: [{ id: "t1", name: "web_search", input: { q: "creators" } }],
195
+ });
196
+ const messages: Message[] = [{ role: "assistant", content: envelope }];
197
+ expect(lastAssistantText(messages)).toBe("Searching for candidates now.");
198
+ });
199
+
200
+ it("walks back past a trailing tool-call turn with no text", () => {
201
+ // The reported bug: subagent ends on a pure tool call (empty text), but it
202
+ // produced a real summary the turn before. We must surface that summary,
203
+ // not an empty string.
204
+ const toolOnly = JSON.stringify({
205
+ text: "",
206
+ tool_calls: [{ id: "t9", name: "web_search", input: { q: "x" } }],
207
+ });
208
+ const messages: Message[] = [
209
+ { role: "user", content: "go" },
210
+ { role: "assistant", content: "Found 12 candidates, here they are: ..." },
211
+ { role: "tool", content: "[]" },
212
+ { role: "assistant", content: toolOnly },
213
+ ];
214
+ expect(lastAssistantText(messages)).toBe("Found 12 candidates, here they are: ...");
215
+ });
216
+
217
+ it("extracts text from ContentPart[] content", () => {
218
+ const messages: Message[] = [
219
+ {
220
+ role: "assistant",
221
+ content: [
222
+ { type: "text", text: "part one" },
223
+ { type: "file", data: "Zm9v", mediaType: "image/png" },
224
+ { type: "text", text: " part two" },
225
+ ],
226
+ },
227
+ ];
228
+ expect(lastAssistantText(messages)).toBe("part one part two");
229
+ });
230
+
231
+ it("returns empty string when there is genuinely no assistant text", () => {
232
+ const messages: Message[] = [
233
+ { role: "user", content: "hi" },
234
+ {
235
+ role: "assistant",
236
+ content: JSON.stringify({ text: "", tool_calls: [{ id: "t1", name: "x", input: {} }] }),
237
+ },
238
+ ];
239
+ expect(lastAssistantText(messages)).toBe("");
240
+ });
241
+ });
242
+
243
+ describe("realResponseText (strips run:error placeholder)", () => {
244
+ it("drops the synthetic [Error: ...] placeholder", () => {
245
+ expect(realResponseText("[Error: Run exceeded timeout of 300s]")).toBe("");
246
+ });
247
+ it("keeps real text and trims", () => {
248
+ expect(realResponseText(" done, wrote the file ")).toBe("done, wrote the file");
249
+ });
250
+ it("handles undefined", () => {
251
+ expect(realResponseText(undefined)).toBe("");
252
+ });
253
+ });
254
+
255
+ describe("abnormalEndResponse (graceful timeout / error delivery)", () => {
256
+ it("timeout WITH gathered work: notes the cutoff and includes the work", () => {
257
+ const out = abnormalEndResponse({
258
+ subagentId: "sub_1",
259
+ gathered: "Found 12 competitors: A, B, C...",
260
+ runError: { code: "TIMEOUT", message: "Run exceeded timeout of 3600s" },
261
+ });
262
+ expect(out).toContain("time limit");
263
+ expect(out).toContain("may not have written its output files");
264
+ expect(out).toContain("Found 12 competitors: A, B, C...");
265
+ expect(out).not.toContain("(no result)");
266
+ });
267
+
268
+ it("timeout WITHOUT gathered work: points at read_subagent to recover", () => {
269
+ const out = abnormalEndResponse({
270
+ subagentId: "sub_2",
271
+ gathered: "",
272
+ runError: { code: "TIMEOUT", message: "Run exceeded timeout of 3600s" },
273
+ });
274
+ expect(out).toContain("time limit");
275
+ expect(out).toContain('read_subagent("sub_2"');
276
+ expect(out).toContain('mode:"full"');
277
+ });
278
+
279
+ it("non-timeout error: surfaces the error message", () => {
280
+ const out = abnormalEndResponse({
281
+ subagentId: "sub_3",
282
+ gathered: "",
283
+ runError: { code: "EMPTY_RESPONSE", message: "model returned no content" },
284
+ });
285
+ expect(out).toContain("ended before finishing");
286
+ expect(out).toContain("model returned no content");
287
+ });
288
+ });