Files
horus-3d/tests/unit/runtimeEventBridge.test.ts
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

641 lines
19 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildHistoryLines,
buildHistorySyncPatch,
buildSummarySnapshotPatches,
classifyGatewayEventKind,
dedupeRunLines,
getAgentSummaryPatch,
getChatSummaryPatch,
isReasoningRuntimeAgentStream,
mergeHistoryWithPending,
mergeRuntimeStream,
resolveAssistantCompletionTimestamp,
resolveLifecyclePatch,
shouldPublishAssistantStream,
} from "@/features/agents/state/runtimeEventBridge";
import { EXEC_APPROVAL_AUTO_RESUME_MARKER } from "@/lib/text/message-extract";
describe("runtime event bridge helpers", () => {
it("classifies gateway events by routing category", () => {
expect(classifyGatewayEventKind("presence")).toBe("summary-refresh");
expect(classifyGatewayEventKind("heartbeat")).toBe("summary-refresh");
expect(classifyGatewayEventKind("chat")).toBe("runtime-chat");
expect(classifyGatewayEventKind("agent")).toBe("runtime-agent");
expect(classifyGatewayEventKind("unknown")).toBe("ignore");
});
it("detects reasoning-like runtime agent streams", () => {
expect(isReasoningRuntimeAgentStream("reasoning")).toBe(true);
expect(isReasoningRuntimeAgentStream("assistant.reasoning")).toBe(true);
expect(isReasoningRuntimeAgentStream("thinking_stream")).toBe(true);
expect(isReasoningRuntimeAgentStream("trace")).toBe(true);
expect(isReasoningRuntimeAgentStream("analysis")).toBe(true);
expect(isReasoningRuntimeAgentStream("assistant")).toBe(false);
expect(isReasoningRuntimeAgentStream("tool")).toBe(false);
expect(isReasoningRuntimeAgentStream("lifecycle")).toBe(false);
});
it("merges assistant stream text deterministically", () => {
expect(mergeRuntimeStream("", "delta")).toBe("delta");
expect(mergeRuntimeStream("hello", "hello world")).toBe("hello world");
expect(mergeRuntimeStream("hello", " world")).toBe("hello world");
expect(mergeRuntimeStream("hello", "hello")).toBe("hello");
});
it("dedupes tool lines per run", () => {
const first = dedupeRunLines(new Set<string>(), ["a", "b", "a"]);
expect(first.appended).toEqual(["a", "b"]);
const second = dedupeRunLines(first.nextSeen, ["b", "c"]);
expect(second.appended).toEqual(["c"]);
});
it("resolves lifecycle transitions with run guards", () => {
const started = resolveLifecyclePatch({
phase: "start",
incomingRunId: "run-1",
currentRunId: null,
lastActivityAt: 123,
});
expect(started.kind).toBe("start");
if (started.kind !== "start") throw new Error("Expected start transition");
expect(started.patch.status).toBe("running");
expect(started.patch.runId).toBe("run-1");
const ignored = resolveLifecyclePatch({
phase: "end",
incomingRunId: "run-2",
currentRunId: "run-1",
lastActivityAt: 456,
});
expect(ignored.kind).toBe("ignore");
const ended = resolveLifecyclePatch({
phase: "end",
incomingRunId: "run-1",
currentRunId: "run-1",
lastActivityAt: 789,
});
expect(ended.kind).toBe("terminal");
if (ended.kind !== "terminal") throw new Error("Expected terminal transition");
expect(ended.patch.status).toBe("idle");
expect(ended.patch.runId).toBeNull();
expect(ended.clearRunTracking).toBe(true);
});
it("suppresses assistant stream publish when chat stream already owns it", () => {
expect(
shouldPublishAssistantStream({
nextText: "hello",
rawText: "",
hasChatEvents: true,
currentStreamText: "already streaming",
})
).toBe(false);
expect(
shouldPublishAssistantStream({
nextText: "hello",
rawText: "",
hasChatEvents: false,
currentStreamText: "already streaming",
})
).toBe(true);
expect(
shouldPublishAssistantStream({
nextText: "",
rawText: "",
hasChatEvents: false,
currentStreamText: null,
})
).toBe(false);
expect(
shouldPublishAssistantStream({
nextText: "already streaming plus more",
rawText: "",
hasChatEvents: true,
currentStreamText: "already streaming",
})
).toBe(true);
});
it("updates preview and activity from assistant chat", () => {
const patch = getChatSummaryPatch(
{
runId: "run-1",
sessionKey: "agent:main:studio:agent-1",
state: "final",
message: { role: "assistant", content: "Hello" },
},
123
);
expect(patch?.latestPreview).toBe("Hello");
expect(patch?.lastActivityAt).toBe(123);
});
it("updates status from agent lifecycle events", () => {
const patch = getAgentSummaryPatch(
{
runId: "run-2",
stream: "lifecycle",
data: { phase: "start" },
},
456
);
expect(patch?.status).toBe("running");
expect(patch?.lastActivityAt).toBe(456);
});
it("resolves assistant completion timestamp only for final assistant messages", () => {
expect(
resolveAssistantCompletionTimestamp({
role: "assistant",
state: "delta",
message: { timestamp: "2024-01-01T00:00:00.000Z" },
})
).toBeNull();
expect(
resolveAssistantCompletionTimestamp({
role: "user",
state: "final",
message: { timestamp: "2024-01-01T00:00:00.000Z" },
})
).toBeNull();
expect(
resolveAssistantCompletionTimestamp({
role: "assistant",
state: "final",
message: { timestamp: "2024-01-01T00:00:00.000Z" },
})
).toBe(Date.parse("2024-01-01T00:00:00.000Z"));
expect(
resolveAssistantCompletionTimestamp({
role: "assistant",
state: "final",
message: {},
now: 1234,
})
).toBe(1234);
});
it("builds summary patches from status and preview snapshots", () => {
const patches = buildSummarySnapshotPatches({
agents: [
{ agentId: "agent-1", sessionKey: "agent:agent-1:studio:session-a" },
{ agentId: "agent-2", sessionKey: "agent:agent-2:studio:session-a" },
],
statusSummary: {
sessions: {
recent: [{ key: "agent:agent-1:studio:session-a", updatedAt: 111 }],
byAgent: [
{
agentId: "agent-2",
recent: [{ key: "agent:agent-2:studio:session-a", updatedAt: 222 }],
},
],
},
},
previewResult: {
ts: 0,
previews: [
{
key: "agent:agent-1:studio:session-a",
status: "ok",
items: [
{ role: "user", text: "Project path: /tmp\n\nhello there" },
{ role: "assistant", text: "assistant latest", timestamp: "not-a-date" },
],
},
],
},
});
expect(patches).toEqual([
{
agentId: "agent-1",
patch: {
lastActivityAt: 111,
lastAssistantMessageAt: 111,
latestPreview: "assistant latest",
lastUserMessage: "hello there",
},
},
{
agentId: "agent-2",
patch: {
lastActivityAt: 222,
},
},
]);
});
it("returns no entries when snapshots produce no patch fields", () => {
const patches = buildSummarySnapshotPatches({
agents: [{ agentId: "agent-1", sessionKey: "agent:agent-1:studio:session-a" }],
statusSummary: { sessions: { recent: [] } },
previewResult: { ts: 0, previews: [] },
});
expect(patches).toEqual([]);
});
it("does not update assistant sort timestamp from summary while agent is running", () => {
const patches = buildSummarySnapshotPatches({
agents: [
{
agentId: "agent-1",
sessionKey: "agent:agent-1:studio:session-a",
status: "running",
},
],
statusSummary: {
sessions: {
recent: [{ key: "agent:agent-1:studio:session-a", updatedAt: 111 }],
},
},
previewResult: {
ts: 0,
previews: [
{
key: "agent:agent-1:studio:session-a",
status: "ok",
items: [{ role: "assistant", text: "assistant latest", timestamp: 999 }],
},
],
},
});
expect(patches).toEqual([
{
agentId: "agent-1",
patch: {
lastActivityAt: 111,
latestPreview: "assistant latest",
},
},
]);
});
it("extracts history lines with heartbeat filtering and preserves canonical repeats", () => {
const history = buildHistoryLines([
{ role: "user", content: "Read HEARTBEAT.md if it exists\nHeartbeat file path: /tmp/HEARTBEAT.md" },
{ role: "user", content: "Project path: /tmp/project\n\nhello there" },
{
role: "assistant",
timestamp: "2024-01-01T00:00:00.000Z",
content: [
{ type: "thinking", thinking: "step one" },
{ type: "text", text: "assistant final" },
],
},
{
role: "assistant",
timestamp: "2024-01-01T00:00:01.000Z",
content: "assistant final",
},
{
role: "toolResult",
toolName: "shell",
toolCallId: "call-1",
details: { status: "ok" },
text: "done",
},
]);
expect(history.lines).toEqual([
"> hello there",
'[[meta]]{"role":"assistant","timestamp":1704067200000}',
"[[trace]]\n_step one_",
"assistant final",
'[[meta]]{"role":"assistant","timestamp":1704067201000}',
"assistant final",
"[[tool-result]] shell (call-1)\nok\n```text\ndone\n```",
]);
expect(history.lastAssistant).toBe("assistant final");
expect(history.lastAssistantAt).toBe(Date.parse("2024-01-01T00:00:01.000Z"));
expect(history.lastRole).toBe("assistant");
expect(history.lastUser).toBe("hello there");
});
it("records aborted assistant terminal messages even when assistant content is empty", () => {
const abortedAt = Date.parse("2024-01-01T00:00:01.000Z");
const history = buildHistoryLines([
{
role: "user",
timestamp: "2024-01-01T00:00:00.000Z",
content: "hi",
},
{
role: "assistant",
timestamp: new Date(abortedAt).toISOString(),
content: [],
stopReason: "aborted",
errorMessage: "Request was aborted",
},
]);
expect(history.lines).toEqual([
'[[meta]]{"role":"user","timestamp":1704067200000}',
"> hi",
'[[meta]]{"role":"assistant","timestamp":1704067201000}',
"Run aborted.",
]);
expect(history.lastAssistant).toBe("Run aborted.");
expect(history.lastAssistantAt).toBe(abortedAt);
expect(history.lastRole).toBe("assistant");
expect(history.lastUser).toBe("hi");
});
it("does not render internal auto-resume user messages in reconstructed history", () => {
const history = buildHistoryLines([
{
role: "user",
content: `[Tue 2026-02-17 12:52 PST] ${EXEC_APPROVAL_AUTO_RESUME_MARKER}
Continue where you left off and finish the task.`,
},
{
role: "assistant",
content: "resumed output",
},
]);
expect(history.lines).toEqual(["resumed output"]);
expect(history.lastUser).toBeNull();
});
it("preserves markdown-rich assistant lines and explicit tool boundaries", () => {
const assistantMarkdown = [
"- item one",
"- item two",
"",
"```json",
'{"ok":true}',
"```",
].join("\n");
const history = buildHistoryLines([
{
role: "assistant",
timestamp: "2024-01-01T00:00:00.000Z",
content: assistantMarkdown,
},
{
role: "assistant",
timestamp: "2024-01-01T00:00:01.000Z",
content: assistantMarkdown,
},
{
role: "toolResult",
toolName: "shell",
toolCallId: "call-2",
details: { status: "ok" },
text: "done",
},
]);
expect(history.lines).toEqual([
'[[meta]]{"role":"assistant","timestamp":1704067200000}',
assistantMarkdown,
'[[meta]]{"role":"assistant","timestamp":1704067201000}',
assistantMarkdown,
"[[tool-result]] shell (call-2)\nok\n```text\ndone\n```",
]);
expect(history.lastAssistant).toBe(assistantMarkdown);
expect(history.lastAssistantAt).toBe(Date.parse("2024-01-01T00:00:01.000Z"));
expect(history.lastRole).toBe("assistant");
});
it("normalizes assistant text in history reconstruction", () => {
const history = buildHistoryLines([
{
role: "assistant",
content: "\n- item one \n\n\n- item two\t \n\n",
},
]);
expect(history.lines).toEqual(["- item one\n\n- item two"]);
expect(history.lastAssistant).toBe("- item one\n\n- item two");
expect(history.lastRole).toBe("assistant");
});
it("merges history lines with pending output order and preserves empty-history behavior", () => {
expect(mergeHistoryWithPending(["a", "c"], ["a", "b", "c"])).toEqual(["a", "b", "c"]);
expect(mergeHistoryWithPending([], ["a", "b"])).toEqual([]);
expect(mergeHistoryWithPending(["a", "b"], [])).toEqual(["a", "b"]);
});
it("collapses duplicate plain assistant lines only when history and pending both contain them", () => {
expect(mergeHistoryWithPending(["> q", "final"], ["> q", "final", "final"])).toEqual([
"> q",
"final",
]);
expect(mergeHistoryWithPending(["> q", "final", "final"], ["> q"])).toEqual([
"> q",
"final",
"final",
]);
});
it("caps overlapping assistant duplicate counts to canonical history counts", () => {
expect(mergeHistoryWithPending(["a", "b", "a"], ["a", "a", "b", "a"])).toEqual([
"a",
"b",
"a",
]);
});
it("preserves repeated tool and meta lines during history merge", () => {
const tool = "[[tool]] shell (call-1)\n```json\n{}\n```";
const meta = '[[meta]]{"role":"assistant","timestamp":1704067200000}';
expect(mergeHistoryWithPending([tool], [tool, tool])).toEqual([tool, tool]);
expect(mergeHistoryWithPending([meta], [meta, meta])).toEqual([meta, meta]);
});
it("builds history sync patches for empty, unchanged, and merged cases", () => {
expect(
buildHistorySyncPatch({
messages: [],
currentLines: ["> hello"],
loadedAt: 100,
status: "idle",
runId: null,
})
).toEqual({ historyLoadedAt: 100 });
const unchanged = buildHistorySyncPatch({
messages: [
{
role: "assistant",
timestamp: "2024-01-01T00:00:00.000Z",
content: "done",
},
],
currentLines: ["done"],
loadedAt: 200,
status: "running",
runId: null,
});
expect(unchanged).toEqual({
outputLines: ['[[meta]]{"role":"assistant","timestamp":1704067200000}', "done"],
lastResult: "done",
latestPreview: "done",
lastAssistantMessageAt: Date.parse("2024-01-01T00:00:00.000Z"),
historyLoadedAt: 200,
status: "idle",
runId: null,
runStartedAt: null,
streamText: null,
thinkingTrace: null,
});
const merged = buildHistorySyncPatch({
messages: [
{ role: "user", content: "hello" },
{
role: "assistant",
timestamp: "2024-01-01T00:00:02.000Z",
content: "assistant final",
},
],
currentLines: ["> hello", "pending line"],
loadedAt: 300,
status: "running",
runId: null,
});
expect(merged).toEqual({
outputLines: ["> hello", "pending line", '[[meta]]{"role":"assistant","timestamp":1704067202000}', "assistant final"],
lastResult: "assistant final",
latestPreview: "assistant final",
lastAssistantMessageAt: Date.parse("2024-01-01T00:00:02.000Z"),
lastUserMessage: "hello",
historyLoadedAt: 300,
status: "idle",
runId: null,
runStartedAt: null,
streamText: null,
thinkingTrace: null,
});
});
it("clears stale running state when history ends with aborted assistant terminal message", () => {
const loadedAt = Date.parse("2024-01-01T00:30:00.000Z");
const patch = buildHistorySyncPatch({
messages: [
{
role: "user",
timestamp: "2024-01-01T00:29:00.000Z",
content: "hi",
},
{
role: "assistant",
timestamp: "2024-01-01T00:29:01.000Z",
content: [],
stopReason: "aborted",
errorMessage: "Request was aborted",
},
],
currentLines: [],
loadedAt,
status: "running",
runId: null,
});
expect(patch).toEqual({
outputLines: [
'[[meta]]{"role":"user","timestamp":1704068940000}',
"> hi",
'[[meta]]{"role":"assistant","timestamp":1704068941000}',
"Run aborted.",
],
lastResult: "Run aborted.",
latestPreview: "Run aborted.",
lastAssistantMessageAt: Date.parse("2024-01-01T00:29:01.000Z"),
lastUserMessage: "hi",
historyLoadedAt: loadedAt,
status: "idle",
runId: null,
runStartedAt: null,
streamText: null,
thinkingTrace: null,
});
});
it("infers a running state from recent user-terminal history after reload", () => {
const loadedAt = Date.parse("2024-01-01T00:30:00.000Z");
const lastUserAt = Date.parse("2024-01-01T00:29:30.000Z");
const patch = buildHistorySyncPatch({
messages: [
{
role: "user",
timestamp: new Date(lastUserAt).toISOString(),
content: "keep going",
},
],
currentLines: [],
loadedAt,
status: "idle",
runId: null,
});
expect(patch).toEqual({
outputLines: ['[[meta]]{"role":"user","timestamp":1704068970000}', "> keep going"],
lastResult: null,
lastUserMessage: "keep going",
historyLoadedAt: loadedAt,
status: "running",
runId: null,
runStartedAt: lastUserAt,
streamText: null,
thinkingTrace: null,
});
});
it("prefers canonical history when optimistic user content differs only by whitespace", () => {
const patch = buildHistorySyncPatch({
messages: [
{
role: "user",
timestamp: "2024-01-01T00:00:03.000Z",
content: "line one line two",
},
],
currentLines: ["> line one\n\nline two"],
loadedAt: 400,
status: "idle",
runId: null,
});
expect(patch).toEqual({
outputLines: ['[[meta]]{"role":"user","timestamp":1704067203000}', "> line one line two"],
lastResult: null,
lastUserMessage: "line one line two",
historyLoadedAt: 400,
});
});
it("collapses optimistic user lines when history carries a post-system timestamp envelope", () => {
const patch = buildHistorySyncPatch({
messages: [
{
role: "user",
timestamp: "2026-02-17T23:39:00.000Z",
content:
"System: [2026-02-17 23:38 UTC] queued\n\n[Tue 2026-02-17 23:39 UTC] Ask me some questions",
},
],
currentLines: ["> Ask me some questions"],
loadedAt: 500,
status: "idle",
runId: null,
});
expect(patch).toEqual({
outputLines: ['[[meta]]{"role":"user","timestamp":1771371540000}', "> Ask me some questions"],
lastResult: null,
lastUserMessage: "Ask me some questions",
historyLoadedAt: 500,
});
});
});