port: sync from Mike/pygienium@595ce48f
This commit is contained in:
137
tests/agent-runner.test.ts
Normal file
137
tests/agent-runner.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* agent-runner.test.ts — unit tests for the session event accumulator.
|
||||
*
|
||||
* `applySessionEvent` runs synchronously inside the SDK's event pipeline; a
|
||||
* throw there crashes the host (the SDK's run-failure path re-emits events
|
||||
* through the same callback → stack overflow), stranding run-state at the
|
||||
* phase boundary. These tests pin the handler to never throw on the partial /
|
||||
* malformed event shapes streaming sessions actually emit.
|
||||
*/
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
applySessionEvent,
|
||||
type SessionEventAccumulator,
|
||||
} from "../src/agent-runner.js";
|
||||
import type { AgentSessionEvent } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
function fresh(): SessionEventAccumulator {
|
||||
return { text: "" };
|
||||
}
|
||||
|
||||
/** Build a typed-as-unknown event so malformed shapes compile in tests. */
|
||||
function event(shape: unknown): AgentSessionEvent {
|
||||
return shape as AgentSessionEvent;
|
||||
}
|
||||
|
||||
describe("applySessionEvent", () => {
|
||||
it("accumulates text_delta stream events in order", () => {
|
||||
const acc = fresh();
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({
|
||||
type: "message_update",
|
||||
assistantMessageEvent: { type: "text_delta", delta: "foo" },
|
||||
}),
|
||||
);
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({
|
||||
type: "message_update",
|
||||
assistantMessageEvent: { type: "text_delta", delta: "bar" },
|
||||
}),
|
||||
);
|
||||
expect(acc.text).toBe("foobar");
|
||||
});
|
||||
|
||||
it("does not throw on a message_update with no assistantMessageEvent", () => {
|
||||
const acc = fresh();
|
||||
expect(() =>
|
||||
applySessionEvent(acc, event({ type: "message_update" })),
|
||||
).not.toThrow();
|
||||
expect(acc.text).toBe("");
|
||||
});
|
||||
|
||||
it("does not throw on a message_update with an unknown event shape", () => {
|
||||
const acc = fresh();
|
||||
expect(() => applySessionEvent(acc, event({ type: "bogus_event" }))).not.toThrow();
|
||||
expect(() => applySessionEvent(acc, event(null))).not.toThrow();
|
||||
expect(() => applySessionEvent(acc, event(undefined))).not.toThrow();
|
||||
expect(acc.text).toBe("");
|
||||
});
|
||||
|
||||
it("captures full text + stopReason from message_end when nothing streamed", () => {
|
||||
const acc = fresh();
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({
|
||||
type: "message_end",
|
||||
message: {
|
||||
role: "assistant",
|
||||
stopReason: "stop",
|
||||
content: [{ type: "text", text: "full report" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(acc.text).toBe("full report");
|
||||
expect(acc.stopReason).toBe("stop");
|
||||
});
|
||||
|
||||
it("does not throw on a message_end with no message", () => {
|
||||
const acc = fresh();
|
||||
expect(() => applySessionEvent(acc, event({ type: "message_end" }))).not.toThrow();
|
||||
expect(acc.text).toBe("");
|
||||
});
|
||||
|
||||
it("records an errorMessage surfaced on the final message", () => {
|
||||
const acc = fresh();
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({
|
||||
type: "message_end",
|
||||
message: { role: "assistant", errorMessage: "upstream 529" },
|
||||
}),
|
||||
);
|
||||
expect(acc.errorMessage).toBe("upstream 529");
|
||||
});
|
||||
|
||||
it("forwards stream-driving events and swallows a throwing forwarder", () => {
|
||||
const originalError = console.error;
|
||||
console.error = () => {};
|
||||
try {
|
||||
const forwarded: string[] = [];
|
||||
const forward = (ev: AgentSessionEvent) => {
|
||||
forwarded.push(ev.type);
|
||||
if (ev.type === "tool_execution_end") throw new Error("renderer boom");
|
||||
};
|
||||
const acc = fresh();
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({ type: "tool_execution_start", toolName: "bash" }),
|
||||
forward,
|
||||
);
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({ type: "tool_execution_end", toolName: "bash" }),
|
||||
forward,
|
||||
);
|
||||
expect(forwarded).toEqual(["tool_execution_start", "tool_execution_end"]);
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
});
|
||||
|
||||
it("does not forward non-stream-driving events", () => {
|
||||
const forwarded: string[] = [];
|
||||
const acc = fresh();
|
||||
applySessionEvent(
|
||||
acc,
|
||||
event({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "x" } }),
|
||||
(ev) => forwarded.push(ev.type),
|
||||
);
|
||||
applySessionEvent(acc, event({ type: "agent_settled" }), (ev) =>
|
||||
forwarded.push(ev.type),
|
||||
);
|
||||
expect(forwarded).toEqual([]);
|
||||
expect(acc.text).toBe("x");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user