mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 01:41:40 +00:00
Merged via squash.
Prepared head SHA: 570b7b9459
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
194 lines
5.4 KiB
TypeScript
194 lines
5.4 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import {
|
|
clearAgentRunContext,
|
|
emitAgentEvent,
|
|
getAgentRunContext,
|
|
onAgentEvent,
|
|
registerAgentRunContext,
|
|
resetAgentRunContextForTest,
|
|
} from "./agent-events.js";
|
|
|
|
type AgentEventsModule = typeof import("./agent-events.js");
|
|
|
|
const agentEventsModuleUrl = new URL("./agent-events.ts", import.meta.url).href;
|
|
|
|
async function importAgentEventsModule(cacheBust: string): Promise<AgentEventsModule> {
|
|
return (await import(`${agentEventsModuleUrl}?t=${cacheBust}`)) as AgentEventsModule;
|
|
}
|
|
|
|
describe("agent-events sequencing", () => {
|
|
test("stores and clears run context", async () => {
|
|
resetAgentRunContextForTest();
|
|
registerAgentRunContext("run-1", { sessionKey: "main" });
|
|
expect(getAgentRunContext("run-1")?.sessionKey).toBe("main");
|
|
clearAgentRunContext("run-1");
|
|
expect(getAgentRunContext("run-1")).toBeUndefined();
|
|
});
|
|
|
|
test("maintains monotonic seq per runId", async () => {
|
|
const seen: Record<string, number[]> = {};
|
|
const stop = onAgentEvent((evt) => {
|
|
const list = seen[evt.runId] ?? [];
|
|
seen[evt.runId] = list;
|
|
list.push(evt.seq);
|
|
});
|
|
|
|
emitAgentEvent({ runId: "run-1", stream: "lifecycle", data: {} });
|
|
emitAgentEvent({ runId: "run-1", stream: "lifecycle", data: {} });
|
|
emitAgentEvent({ runId: "run-2", stream: "lifecycle", data: {} });
|
|
emitAgentEvent({ runId: "run-1", stream: "lifecycle", data: {} });
|
|
|
|
stop();
|
|
|
|
expect(seen["run-1"]).toEqual([1, 2, 3]);
|
|
expect(seen["run-2"]).toEqual([1]);
|
|
});
|
|
|
|
test("preserves compaction ordering on the event bus", async () => {
|
|
const phases: Array<string> = [];
|
|
const stop = onAgentEvent((evt) => {
|
|
if (evt.runId !== "run-1") {
|
|
return;
|
|
}
|
|
if (evt.stream !== "compaction") {
|
|
return;
|
|
}
|
|
if (typeof evt.data?.phase === "string") {
|
|
phases.push(evt.data.phase);
|
|
}
|
|
});
|
|
|
|
emitAgentEvent({ runId: "run-1", stream: "compaction", data: { phase: "start" } });
|
|
emitAgentEvent({
|
|
runId: "run-1",
|
|
stream: "compaction",
|
|
data: { phase: "end", willRetry: false },
|
|
});
|
|
|
|
stop();
|
|
|
|
expect(phases).toEqual(["start", "end"]);
|
|
});
|
|
|
|
test("omits sessionKey for runs hidden from Control UI", async () => {
|
|
resetAgentRunContextForTest();
|
|
registerAgentRunContext("run-hidden", {
|
|
sessionKey: "session-imessage",
|
|
isControlUiVisible: false,
|
|
});
|
|
|
|
let receivedSessionKey: string | undefined;
|
|
const stop = onAgentEvent((evt) => {
|
|
receivedSessionKey = evt.sessionKey;
|
|
});
|
|
emitAgentEvent({
|
|
runId: "run-hidden",
|
|
stream: "assistant",
|
|
data: { text: "hi" },
|
|
sessionKey: "session-imessage",
|
|
});
|
|
stop();
|
|
|
|
expect(receivedSessionKey).toBeUndefined();
|
|
});
|
|
|
|
test("merges later run context updates into existing runs", async () => {
|
|
resetAgentRunContextForTest();
|
|
registerAgentRunContext("run-ctx", {
|
|
sessionKey: "session-main",
|
|
isControlUiVisible: true,
|
|
});
|
|
registerAgentRunContext("run-ctx", {
|
|
verboseLevel: "full",
|
|
isHeartbeat: true,
|
|
});
|
|
|
|
expect(getAgentRunContext("run-ctx")).toEqual({
|
|
sessionKey: "session-main",
|
|
verboseLevel: "full",
|
|
isHeartbeat: true,
|
|
isControlUiVisible: true,
|
|
});
|
|
});
|
|
|
|
test("falls back to registered sessionKey when event sessionKey is blank", async () => {
|
|
resetAgentRunContextForTest();
|
|
registerAgentRunContext("run-ctx", { sessionKey: "session-main" });
|
|
|
|
let receivedSessionKey: string | undefined;
|
|
const stop = onAgentEvent((evt) => {
|
|
receivedSessionKey = evt.sessionKey;
|
|
});
|
|
emitAgentEvent({
|
|
runId: "run-ctx",
|
|
stream: "assistant",
|
|
data: { text: "hi" },
|
|
sessionKey: " ",
|
|
});
|
|
stop();
|
|
|
|
expect(receivedSessionKey).toBe("session-main");
|
|
});
|
|
|
|
test("keeps notifying later listeners when one throws", async () => {
|
|
const seen: string[] = [];
|
|
const stopBad = onAgentEvent(() => {
|
|
throw new Error("boom");
|
|
});
|
|
const stopGood = onAgentEvent((evt) => {
|
|
seen.push(evt.runId);
|
|
});
|
|
|
|
expect(() =>
|
|
emitAgentEvent({
|
|
runId: "run-safe",
|
|
stream: "assistant",
|
|
data: { text: "hi" },
|
|
}),
|
|
).not.toThrow();
|
|
|
|
stopGood();
|
|
stopBad();
|
|
|
|
expect(seen).toEqual(["run-safe"]);
|
|
});
|
|
|
|
test("shares run context, listeners, and sequence state across duplicate module instances", async () => {
|
|
const first = await importAgentEventsModule(`first-${Date.now()}`);
|
|
const second = await importAgentEventsModule(`second-${Date.now()}`);
|
|
|
|
first.resetAgentEventsForTest();
|
|
first.registerAgentRunContext("run-dup", { sessionKey: "session-dup" });
|
|
|
|
const seen: Array<{ seq: number; sessionKey?: string }> = [];
|
|
const stop = first.onAgentEvent((evt) => {
|
|
if (evt.runId === "run-dup") {
|
|
seen.push({ seq: evt.seq, sessionKey: evt.sessionKey });
|
|
}
|
|
});
|
|
|
|
second.emitAgentEvent({
|
|
runId: "run-dup",
|
|
stream: "assistant",
|
|
data: { text: "from second" },
|
|
sessionKey: " ",
|
|
});
|
|
first.emitAgentEvent({
|
|
runId: "run-dup",
|
|
stream: "assistant",
|
|
data: { text: "from first" },
|
|
sessionKey: " ",
|
|
});
|
|
|
|
stop();
|
|
|
|
expect(second.getAgentRunContext("run-dup")).toEqual({ sessionKey: "session-dup" });
|
|
expect(seen).toEqual([
|
|
{ seq: 1, sessionKey: "session-dup" },
|
|
{ seq: 2, sessionKey: "session-dup" },
|
|
]);
|
|
|
|
first.resetAgentEventsForTest();
|
|
});
|
|
});
|