evOo-local/apps/web/src/lib/thread-events.test.ts
cynarAI 49431def83
feat: show the bot's live cursor and make the in-app browser work (#113)
Bots now announce every click and typing step before it runs, and the
web app draws a cursor in the bot's colour that glides to that point
over the live computer screen, ripples on clicks and shows typing.

- contracts: ComputerPointerPayload on the existing thread.computer event
- executor: computer_act and browser_act announce pointer steps
  (best effort); the page-browser helper reports element screen boxes,
  which are stripped before results reach the model
- web: agentPointerFeed + AgentCursor over the side-panel preview and
  the full-window screen, mapped through the noVNC letterbox
- in-app browser: native WebContentsView on desktop (own session, http(s)
  only, clamped bounds, back/forward, reload, DevTools, load errors) and
  a sandboxed iframe on the web; replaces the dead panel, the fake
  50%/40% cursors and the unimplemented devServer bridge


Claude-Session: https://claude.ai/code/session_01WiGooXtsKrZQmdszepRrNi

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-03 19:08:48 +02:00

1625 lines
49 KiB
TypeScript

import type {
ComputerStatus,
ProductEvent,
ThreadMessage,
ThreadSnapshot,
} from "@rakazo/contracts";
import { describe, expect, it } from "vitest";
import {
activeThreadRuns,
applyThreadSendReceipt,
clearActiveThreadRuns,
computerPanelAutoBoot,
computerPanelAutoUsesBoot,
computerPanelNeedsMaintenance,
computerTakeoverBlocked,
isThreadSnapshotEvent,
mergeThreadSnapshot,
prependThreadMessagePage,
reconcileRefreshedThread,
reduceComputerStatus,
reduceThreadSnapshot,
threadRunError,
userHoldsComputerControl,
} from "./thread-events.js";
describe("thread event reduction", () => {
it("shows a committed direct send as queued before its snapshot refresh returns", () => {
const initial = snapshot([message("user-1", [{ kind: "text", text: "Continue" }], 4)]);
const next = applyThreadSendReceipt(initial, {
botId: "bot-1",
runId: "run-receipt",
taskId: "task-receipt",
createdAt: "2026-09-03T21:29:52.000Z",
});
expect(next?.run).toMatchObject({
id: "run-receipt",
taskId: "task-receipt",
status: "queued",
});
expect(next?.activeRuns).toEqual([next?.run]);
expect(next?.messages).toBe(initial.messages);
});
it("does not replace authoritative active or group run state with a send receipt", () => {
const active = threadRun("run-active");
const direct: ThreadSnapshot = { ...snapshot([]), run: active, activeRuns: [active] };
const group: ThreadSnapshot = { ...snapshot([]), groupId: "group-1" };
const receipt = { botId: "bot-1", runId: "run-new", taskId: "task-new" };
const completed = { ...threadRun(receipt.runId), status: "completed" as const };
expect(applyThreadSendReceipt(direct, receipt)).toBe(direct);
expect(applyThreadSendReceipt(group, receipt)).toBe(group);
expect(applyThreadSendReceipt({ ...snapshot([]), run: completed }, receipt)?.run).toBe(
completed,
);
expect(applyThreadSendReceipt(snapshot([]), receipt, new Set([receipt.runId]))).toEqual(
snapshot([]),
);
});
it("appends an emoji reply with its exact target", () => {
const initial = snapshot([message("message-1", [{ kind: "text", text: "Done" }], 1)]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.created",
seq: 4,
payload: {
messageId: "reaction-1",
role: "user",
blocks: [{ kind: "text", text: "❤️" }],
replyToMessageId: "message-1",
},
}),
);
expect(next?.messages.find((message) => message.id === "reaction-1")).toMatchObject({
role: "user",
blocks: [{ kind: "text", text: "❤️" }],
replyToMessageId: "message-1",
});
expect(next?.cursor).toBe(4);
});
it("appends a quoted reply carrying its excerpt", () => {
const initial = snapshot([message("message-1", [{ kind: "text", text: "Done" }], 1)]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.created",
seq: 4,
payload: {
messageId: "reply-1",
role: "user",
blocks: [{ kind: "text", text: "why this?" }],
replyToMessageId: "message-1",
replyQuote: "Done",
},
}),
);
expect(next?.messages.find((message) => message.id === "reply-1")).toMatchObject({
role: "user",
replyToMessageId: "message-1",
replyQuote: "Done",
});
});
it("prepends older pages in order, removes overlaps, and advances the history cursor", () => {
const initial = snapshot([message("m-2", [], 2), message("m-3", [], 3)], 2);
const next = prependThreadMessagePage(initial, {
threadId: "thread-1",
messages: [message("m-0", [], 0), message("m-1", [], 1), message("m-2", [], 2)],
olderCursor: null,
});
expect(next?.messages.map((item) => item.id)).toEqual(["m-0", "m-1", "m-2", "m-3"]);
expect(next?.olderCursor).toBeNull();
});
it("ignores a stale older page after the conversation was cleared", () => {
const cleared = snapshot([], null);
const next = prependThreadMessagePage(cleared, {
threadId: "thread-1",
messages: [message("old-1", [], 0), message("old-2", [], 1)],
olderCursor: null,
});
expect(next).toBe(cleared);
});
it("merges a refreshed recent page with loaded history and drops stale live messages", () => {
const previous = snapshot(
[
message("m-0", [], 0),
message("m-1", [], 1),
message("progress:run-1", [{ kind: "progress", text: "draft" }], 9),
],
null,
);
const recent = snapshot([message("m-1", [], 1), message("m-2", [], 2)], 1);
const next = mergeThreadSnapshot(previous, recent, true);
expect(next.messages.map((item) => item.id)).toEqual(["m-0", "m-1", "m-2"]);
expect(next.olderCursor).toBeNull();
});
it("ignores a stale thread refresh that is behind the live cursor", () => {
const live: ThreadSnapshot = {
...snapshot([
message("ask-1", [{ kind: "ask", text: "Which city?", status: "pending" }], 11),
]),
cursor: 12,
};
const stale: ThreadSnapshot = {
...snapshot([message("m-1", [{ kind: "text", text: "older" }], 1)]),
cursor: 8,
};
expect(mergeThreadSnapshot(live, stale)).toBe(live);
expect(mergeThreadSnapshot(live, stale, true)).toBe(live);
});
it("accumulates progress deltas and keeps only the active progress message", () => {
const stale = message("progress:older", [{ kind: "progress", text: "old run" }]);
const initial = snapshot([stale]);
const first = reduceThreadSnapshot(
initial,
event({ type: "thread.progress", seq: 4, runId: "run-1", payload: { delta: "Hel" } }),
);
const second = reduceThreadSnapshot(
first,
event({ type: "thread.progress", seq: 5, runId: "run-1", payload: { delta: "lo" } }),
);
expect(second?.cursor).toBe(5);
expect(second?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [{ kind: "progress", text: "Hello" }],
}),
]);
});
it("updates a live subagent in place while preserving streamed answer progress", () => {
const activeRun = threadRun("run-1");
const initial: ThreadSnapshot = {
...snapshot([
message("subagent:research", [
{
kind: "subagent",
agentId: "research",
name: "Research",
task: "Find sources",
status: "running",
progress: "Starting",
},
]),
{
...message("progress:run-1", [{ kind: "progress", text: "Draft" }]),
runId: activeRun.id,
},
]),
run: activeRun,
activeRuns: undefined,
};
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.subagent",
seq: 8,
payload: {
agentId: "research",
name: "Research",
task: "Find sources",
status: "completed",
result: "Three sources found",
},
}),
);
expect(next?.messages.map((item) => item.id)).toEqual(["subagent:research", "progress:run-1"]);
expect(next?.messages[0]?.blocks[0]).toMatchObject({
kind: "subagent",
status: "completed",
result: "Three sources found",
});
});
it("replaces transient progress and a matching live subagent with the durable message", () => {
const initial = snapshot([
message("durable", [{ kind: "text", text: "old value" }]),
message("subagent:research", [
{
kind: "subagent",
agentId: "research",
name: "Research",
task: "Find sources",
status: "running",
},
]),
message("subagent:other", [
{
kind: "subagent",
agentId: "other",
name: "Other",
task: "Keep working",
status: "running",
},
]),
message("progress:run-1", [{ kind: "progress", text: "Draft" }]),
]);
const completedBlock = {
kind: "subagent" as const,
agentId: "research",
name: "Research",
task: "Find sources",
status: "completed" as const,
result: "Done",
};
const next = reduceThreadSnapshot(
initial,
event({
id: "event-message",
type: "thread.message.created",
seq: 9,
payload: { messageId: "durable", role: "bot", blocks: [completedBlock] },
}),
);
expect(next?.messages.map((item) => item.id)).toEqual(["durable", "subagent:other"]);
expect(next?.messages[0]?.blocks).toEqual([completedBlock]);
});
it("keeps a replayed bot-to-bot marker in its durable transcript position", () => {
const peerBlock = {
kind: "bot_message_received" as const,
fromBotId: "bot-peer",
fromBotName: "Peer",
text: "Please check this.",
};
const initial = snapshot([
message("peer-message", [peerBlock], 1),
message("newer-message", [{ kind: "text", text: "Working on it." }], 2),
]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.created",
seq: 9,
payload: { messageId: "peer-message", role: "user", blocks: [peerBlock] },
}),
);
expect(next?.messages.map((item) => item.id)).toEqual(["peer-message", "newer-message"]);
});
it("clears durable and transient history when another client clears the thread", () => {
const initial = snapshot(
[
message("message-1", [{ kind: "text", text: "old" }]),
message("progress:run-1", [{ kind: "progress", text: "draft" }]),
],
1,
);
initial.run = {
id: "run-1",
botId: "bot-1",
threadId: "thread-1",
taskId: "task-1",
status: "running",
trigger: "user",
routineId: null,
modelProvider: null,
modelId: null,
error: null,
startedAt: null,
completedAt: null,
createdAt: "2026-08-16T00:00:00.000Z",
};
const next = reduceThreadSnapshot(
initial,
event({ type: "thread.cleared", seq: 12, runId: undefined }),
);
expect(next).toMatchObject({ cursor: 12, messages: [], olderCursor: null, run: null });
});
it("routes clear and terminal events through the snapshot reducer", () => {
expect(
isThreadSnapshotEvent(event({ type: "thread.cleared", seq: 12, runId: undefined })),
).toBe(true);
expect(isThreadSnapshotEvent(event({ type: "run.started" }))).toBe(true);
expect(isThreadSnapshotEvent(event({ type: "run.completed" }))).toBe(true);
expect(isThreadSnapshotEvent(event({ type: "computer.takeover.requested" }))).toBe(true);
expect(isThreadSnapshotEvent(event({ type: "agent.tool.completed" }))).toBe(true);
});
it("event-sources the active run on run.started so Stop does not wait on threads.get", () => {
const initial = snapshot([]);
const started = reduceThreadSnapshot(
initial,
event({
type: "run.started",
seq: 3,
runId: "run-1",
payload: { trigger: "user" },
}),
);
expect(started?.run).toMatchObject({
id: "run-1",
botId: "bot-1",
status: "running",
trigger: "user",
});
expect(started?.activeRuns).toEqual([started?.run]);
const progressed = reduceThreadSnapshot(
started,
event({
type: "thread.progress",
seq: 4,
runId: "run-1",
payload: { delta: "still working" },
}),
);
expect(progressed?.run?.id).toBe("run-1");
expect(progressed?.cursor).toBe(4);
});
it("preserves bot_message when event-sourcing a peer run", () => {
const started = reduceThreadSnapshot(
snapshot([]),
event({
type: "run.started",
runId: "peer-run-1",
payload: { trigger: "bot_message" },
}),
);
expect(started?.run?.trigger).toBe("bot_message");
});
it("preserves webhook when event-sourcing an inbound wake", () => {
const started = reduceThreadSnapshot(
snapshot([]),
event({
type: "run.started",
runId: "webhook-run-1",
payload: { trigger: "webhook" },
}),
);
expect(started?.run?.trigger).toBe("webhook");
});
it("preserves delegation when event-sourcing a delegated task run", () => {
const started = reduceThreadSnapshot(
snapshot([]),
event({
type: "run.started",
runId: "task-run-1",
payload: { trigger: "delegation" },
}),
);
expect(started?.run?.trigger).toBe("delegation");
});
it("marks the run as waiting when computer takeover is requested", () => {
const run = threadRun("run-1");
const initial: ThreadSnapshot = {
...snapshot([]),
run,
activeRuns: [run],
};
const waiting = reduceThreadSnapshot(
initial,
event({ type: "computer.takeover.requested", seq: 5, runId: run.id }),
);
expect(waiting?.run?.status).toBe("waiting_takeover");
expect(waiting?.activeRuns?.[0]?.status).toBe("waiting_takeover");
});
it("inserts a peer takeover run that was absent from the open snapshot", () => {
const userRun = threadRun("run-user");
const initial: ThreadSnapshot = {
...snapshot([]),
run: userRun,
activeRuns: [userRun],
};
const waiting = reduceThreadSnapshot(
initial,
event({
type: "computer.takeover.requested",
seq: 12,
runId: "run-peer",
botId: "bot-peer",
}),
);
expect(waiting?.run).toMatchObject({
id: "run-peer",
botId: "bot-peer",
status: "waiting_takeover",
trigger: "bot_message",
});
expect(waiting?.activeRuns?.map((run) => ({ id: run.id, status: run.status }))).toEqual([
{ id: "run-user", status: "running" },
{ id: "run-peer", status: "waiting_takeover" },
]);
});
it("keeps event-sourced waiting_takeover when a stale refresh still shows the bot busy", () => {
const run = threadRun("run-1");
const waitingLocal: ThreadSnapshot = {
...snapshot([]),
cursor: 10,
run: { ...run, status: "waiting_takeover" },
activeRuns: [{ ...run, status: "waiting_takeover" }],
};
const staleRefresh: ThreadSnapshot = {
...snapshot([]),
cursor: 10,
run,
activeRuns: [run],
computer: computer({ state: "running", busyBotName: "Chief" }),
};
const reconciled = reconcileRefreshedThread(
waitingLocal,
staleRefresh,
computer({ state: "running", busyBotName: null }),
);
expect(reconciled.snapshot.run?.status).toBe("waiting_takeover");
expect(reconciled.computer?.busyBotName).toBeNull();
expect(computerTakeoverBlocked(reconciled.computer, reconciled.snapshot.run?.status)).toBe(
false,
);
});
it("ignores a refresh whose cursor is behind the event-sourced snapshot", () => {
const run = threadRun("run-1");
const newer: ThreadSnapshot = {
...snapshot([]),
cursor: 12,
run: { ...run, status: "waiting_takeover" },
};
const older: ThreadSnapshot = {
...snapshot([]),
cursor: 9,
run,
computer: computer({ busyBotName: "Chief" }),
};
const prevComputer = computer({ busyBotName: null });
const reconciled = reconcileRefreshedThread(newer, older, prevComputer);
expect(reconciled.snapshot).toBe(newer);
expect(reconciled.computer).toBe(prevComputer);
});
it("refreshes busy status when only transient thread events are ahead", () => {
const run = threadRun("run-1");
const newer: ThreadSnapshot = { ...snapshot([]), cursor: 12, run };
const older: ThreadSnapshot = {
...snapshot([]),
cursor: 9,
run,
computer: computer({ state: "running", controlHolder: "bot", busyBotName: "Chief" }),
};
const reconciled = reconcileRefreshedThread(
newer,
older,
computer({ state: "running", controlHolder: "bot", busyBotName: null }),
);
expect(reconciled.snapshot).toBe(newer);
expect(reconciled.computer?.busyBotName).toBe("Chief");
});
it("hydrates a live run from a refresh without rolling back newer progress", () => {
const run = threadRun("run-1");
const liveProgress = {
...message("progress:run-1", [{ kind: "progress" as const, text: "Still working" }]),
runId: run.id,
};
const newer: ThreadSnapshot = { ...snapshot([liveProgress]), cursor: 12, run: null };
const older: ThreadSnapshot = {
...snapshot([]),
cursor: 11,
run,
computer: computer({ state: "running", controlHolder: "bot", busyBotName: "Chief" }),
};
const reconciled = reconcileRefreshedThread(newer, older, computer());
expect(reconciled.snapshot.cursor).toBe(12);
expect(reconciled.snapshot.messages).toEqual([liveProgress]);
expect(reconciled.snapshot.run).toBe(run);
expect(reconciled.computer?.busyBotName).toBe("Chief");
});
it("clears active runs and their transient progress after stop", () => {
const run = threadRun("run-1");
const durable = message("message-1", [{ kind: "text", text: "Keep me" }]);
const progress = {
...message("progress:run-1", [{ kind: "progress" as const, text: "Still working" }]),
runId: run.id,
};
const stopped = clearActiveThreadRuns({
...snapshot([durable, progress]),
run,
activeRuns: [run],
computer: computer({ state: "running", busyBotName: "Chief" }),
});
expect(stopped.run).toBeNull();
expect(stopped.activeRuns).toEqual([]);
expect(stopped.messages).toEqual([durable]);
expect(stopped.computer?.busyBotName).toBeNull();
});
it("keeps an optimistic stop clear when an older cursor refresh still looks busy", () => {
// Stop has no terminal event, so progress can leave the local cursor ahead of threads.get.
// After the shell clears run/busy locally, that older get must not restore Stop / Take control block.
const run = threadRun("run-1");
const stoppedLocal: ThreadSnapshot = {
...snapshot([]),
cursor: 15,
run: null,
activeRuns: [],
computer: computer({ state: "running", busyBotName: null }),
};
const staleBusyRefresh: ThreadSnapshot = {
...snapshot([]),
cursor: 10,
run,
activeRuns: [run],
computer: computer({ state: "running", busyBotName: "Chief" }),
};
const clearedComputer = computer({ state: "running", busyBotName: null });
const reconciled = reconcileRefreshedThread(stoppedLocal, staleBusyRefresh, clearedComputer);
expect(reconciled.snapshot.run).toBeNull();
expect(reconciled.snapshot.activeRuns).toEqual([]);
expect(reconciled.computer?.busyBotName).toBeNull();
expect(computerTakeoverBlocked(reconciled.computer, reconciled.snapshot.run?.status)).toBe(
false,
);
});
it("always replaces the snapshot when switching to a different thread", () => {
const previous: ThreadSnapshot = {
...snapshot([]),
threadId: "thread-writer",
cursor: 40,
computer: computer({ mode: "team", busyBotName: null }),
};
const next: ThreadSnapshot = {
...snapshot([]),
botId: "bot-private",
threadId: "thread-private",
cursor: 0,
computer: computer({ mode: "dedicated", state: "running" }),
};
const reconciled = reconcileRefreshedThread(previous, next, previous.computer ?? null);
expect(reconciled.snapshot.threadId).toBe("thread-private");
expect(reconciled.computer?.mode).toBe("dedicated");
});
it("keeps group member status in sync with run lifecycle events", () => {
const run = threadRun("run-1", "bot-member");
const initial: ThreadSnapshot = {
...snapshot([]),
botId: undefined,
groupId: "group-1",
members: [{ botId: "bot-member", name: "Member", color: "#8B5CF6", status: "idle" }],
};
const started = reduceThreadSnapshot(
initial,
event({ type: "run.started", botId: "bot-member", runId: run.id }),
);
expect(started?.members?.[0]?.status).toBe("running");
expect(started?.run?.id).toBe(run.id);
expect(started?.run?.status).toBe("running");
expect(started?.activeRuns?.map((item) => item.id)).toEqual([run.id]);
const waiting = reduceThreadSnapshot(
started!,
event({
type: "run.waiting_input",
seq: 5,
botId: "bot-member",
runId: run.id,
}),
);
expect(waiting?.members?.[0]?.status).toBe("waiting_input");
const completed = reduceThreadSnapshot(
waiting,
event({ type: "run.completed", seq: 6, botId: "bot-member", runId: run.id }),
);
expect(completed?.members?.[0]?.status).toBe("idle");
});
it("clears only the terminal run's live progress", () => {
const runA = threadRun("run-a", "bot-a");
const runB = threadRun("run-b", "bot-b");
const initial: ThreadSnapshot = {
...snapshot([
{
...message("progress:run-a", [{ kind: "progress", text: "A" }]),
runId: runA.id,
},
{
...message("progress:run-b", [{ kind: "progress", text: "B" }]),
runId: runB.id,
},
]),
run: runA,
activeRuns: [runA, runB],
};
const failed = event({ type: "run.failed", seq: 10, runId: runA.id });
const next = reduceThreadSnapshot(initial, failed);
expect(isThreadSnapshotEvent(failed)).toBe(true);
expect(next?.messages.map((item) => item.id)).toEqual(["progress:run-b"]);
expect(next?.run).toEqual(runB);
expect(next?.activeRuns).toEqual([runB]);
expect(next?.cursor).toBe(10);
});
it("keeps a failed run and its error so the thread can surface the failure", () => {
const failing = threadRun("run-a");
const initial: ThreadSnapshot = {
...snapshot([
{ ...message("progress:run-a", [{ kind: "progress", text: "A" }]), runId: failing.id },
]),
run: failing,
};
const failed = event({
type: "run.failed",
seq: 11,
runId: failing.id,
payload: { error: "Provider is not configured: openrouter" },
});
const next = reduceThreadSnapshot(initial, failed);
expect(next?.messages).toEqual([]);
expect(next?.run).toMatchObject({
id: failing.id,
status: "failed",
error: "Provider is not configured: openrouter",
});
expect(threadRunError(next)).toBe("Provider is not configured: openrouter");
expect(threadRunError(next, new Set([failing.id]))).toBeNull();
expect(threadRunError(next, new Set(["other-run"]))).toBe(
"Provider is not configured: openrouter",
);
});
it("keeps the error when a member run fails while another member is still running", () => {
const runA = threadRun("run-a", "bot-a");
const runB = threadRun("run-b", "bot-b");
const initial: ThreadSnapshot = {
...snapshot([]),
run: runA,
activeRuns: [runA, runB],
};
const next = reduceThreadSnapshot(
initial,
event({
type: "run.failed",
seq: 13,
botId: "bot-b",
runId: runB.id,
payload: { error: "member exploded" },
}),
);
expect(next?.activeRuns).toEqual([runA]);
expect(next?.run).toMatchObject({ id: runB.id, status: "failed", error: "member exploded" });
expect(threadRunError(next)).toBe("member exploded");
});
it("keeps a group failure visible when another member starts before dismiss", () => {
const failed = {
...threadRun("run-b", "bot-b"),
status: "failed" as const,
error: "member exploded",
};
const runA = threadRun("run-a", "bot-a");
const initial: ThreadSnapshot = {
...snapshot([]),
groupId: "group-1",
run: failed,
activeRuns: [runA],
};
const next = reduceThreadSnapshot(
initial,
event({
type: "run.started",
seq: 14,
botId: "bot-c",
runId: "run-c",
}),
);
expect(next?.run).toMatchObject({ id: failed.id, status: "failed", error: "member exploded" });
expect(next?.activeRuns).toEqual([
runA,
expect.objectContaining({ id: "run-c", botId: "bot-c", status: "running" }),
]);
expect(threadRunError(next)).toBe("member exploded");
});
it("clears the run and reports no error when it completes or fails without a message", () => {
const finishing = threadRun("run-a");
const initial: ThreadSnapshot = { ...snapshot([]), run: finishing };
const completed = reduceThreadSnapshot(
initial,
event({ type: "run.completed", seq: 12, runId: finishing.id }),
);
const blank = reduceThreadSnapshot(
initial,
event({ type: "run.failed", seq: 12, runId: finishing.id, payload: { error: " " } }),
);
expect(completed?.run).toBeNull();
expect(threadRunError(completed)).toBeNull();
expect(blank?.run).toBeNull();
expect(threadRunError(blank)).toBeNull();
});
it("applies the durable waiting-input run transition without a refresh", () => {
const initial: ThreadSnapshot = {
...snapshot([]),
run: {
id: "run-1",
botId: "bot-1",
threadId: "thread-1",
taskId: "task-1",
status: "running",
trigger: "user",
routineId: null,
modelProvider: null,
modelId: null,
error: null,
startedAt: null,
completedAt: null,
createdAt: "2026-08-16T00:00:00.000Z",
},
};
const waiting = reduceThreadSnapshot(
initial,
event({ type: "run.waiting_input", seq: 6, runId: "run-1" }),
);
expect(waiting?.run?.status).toBe("waiting_input");
expect(waiting?.cursor).toBe(6);
expect(
reduceThreadSnapshot(waiting, event({ type: "run.waiting_input", seq: 7, runId: "run-1" })),
).toBe(waiting);
});
it("clears live progress when a run waits for input", () => {
const run = threadRun("run-1");
const initial: ThreadSnapshot = {
...snapshot([
{
...message("progress:run-1", [{ kind: "progress", text: "working…" }]),
runId: run.id,
},
]),
run,
activeRuns: [run],
};
const waiting = reduceThreadSnapshot(
initial,
event({ type: "run.waiting_input", seq: 6, runId: run.id }),
);
expect(waiting?.messages).toEqual([]);
expect(waiting?.run?.status).toBe("waiting_input");
expect(waiting?.activeRuns?.[0]?.status).toBe("waiting_input");
});
it("accumulates tool-call steps and collapses repeats into a count", () => {
const initial = snapshot([]);
const first = reduceThreadSnapshot(
initial,
event({
type: "agent.tool.called",
seq: 4,
runId: "run-1",
payload: { name: "SLACK_FIND_CHANNELS" },
}),
);
const second = reduceThreadSnapshot(
first,
event({
type: "agent.tool.called",
seq: 5,
runId: "run-1",
payload: { name: "SLACK_FETCH_CONVERSATION_HISTORY" },
}),
);
const third = reduceThreadSnapshot(
second,
event({
type: "agent.tool.called",
seq: 6,
runId: "run-1",
payload: { name: "SLACK_FETCH_CONVERSATION_HISTORY" },
}),
);
expect(third?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [
{
kind: "steps",
steps: [
{ label: "Slack find channels", count: 1 },
{ label: "Slack fetch conversation history", count: 2 },
],
},
],
}),
]);
});
it("advances past tool completion audit events without adding a visible message", () => {
const initial = snapshot(
[
message(
"progress:run-1",
[{ kind: "steps", steps: [{ label: "Slack find channels", count: 1 }] }],
4,
),
],
4,
);
const next = reduceThreadSnapshot(
initial,
event({
type: "agent.tool.completed",
seq: 5,
runId: "run-1",
payload: { name: "SLACK_FIND_CHANNELS", outcome: "succeeded" },
}),
);
expect(next?.cursor).toBe(5);
expect(next?.messages).toEqual(initial.messages);
});
it("advances past a bot pointer announcement without touching messages", () => {
const initial = snapshot([message("m1", [{ kind: "text", text: "hi" }], 3)], 3);
const pointer = event({
type: "thread.computer",
seq: 4,
runId: "run-1",
payload: {
kind: "pointer",
screen: { width: 1280, height: 800 },
steps: [{ action: "click", x: 10, y: 20 }],
},
});
expect(isThreadSnapshotEvent(pointer)).toBe(true);
const next = reduceThreadSnapshot(initial, pointer);
expect(next?.cursor).toBe(4);
expect(next?.messages).toEqual(initial.messages);
});
it("holds a tool call that lands mid-sentence until the sentence completes", () => {
const initial = snapshot([]);
const afterNarration = reduceThreadSnapshot(
initial,
event({
type: "thread.progress",
seq: 4,
runId: "run-1",
payload: { text: "Let me check Slack ", streaming: true },
}),
);
const afterTool = reduceThreadSnapshot(
afterNarration,
event({
type: "agent.tool.called",
seq: 5,
runId: "run-1",
payload: { name: "SLACK_FIND_CHANNELS" },
}),
);
// Still mid-sentence — the tool call stays hidden, folded into the streaming text instead.
expect(afterTool?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [
{
kind: "progress",
text: "Let me check Slack ",
pendingToolNames: ["SLACK_FIND_CHANNELS"],
},
],
}),
]);
const afterMore = reduceThreadSnapshot(
afterTool,
event({
type: "thread.progress",
seq: 6,
runId: "run-1",
payload: { delta: "for a broad search.", streaming: true },
}),
);
// The sentence just finished — the completed sentence and the held-back tool call appear
// together, in that order.
expect(afterMore?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [
{ kind: "text", text: "Let me check Slack for a broad search." },
{ kind: "steps", steps: [{ label: "Slack find channels", count: 1 }] },
],
}),
]);
});
it("survives a React StrictMode replay of the same event without double-counting", () => {
// StrictMode invokes a setState updater twice per event in development to catch impure
// updaters — reduceThreadSnapshot(prev, event) must return the same result both times
// rather than mutating its pending-tool-call bookkeeping a second time.
const initial = snapshot([]);
const afterNarration = reduceThreadSnapshot(
initial,
event({
type: "thread.progress",
seq: 4,
runId: "run-1",
payload: { text: "Let me check ", streaming: true },
}),
);
const toolEvent = event({
type: "agent.tool.called",
seq: 5,
runId: "run-1",
payload: { name: "SLACK_FIND_CHANNELS" },
});
// The tool call lands mid-sentence, so it's held back rather than flushed immediately —
// exactly the state a naive module-level mutation would double-push on replay.
const first = reduceThreadSnapshot(afterNarration, toolEvent);
const replay = reduceThreadSnapshot(afterNarration, toolEvent);
expect(replay).toEqual(first);
const sentenceEnd = reduceThreadSnapshot(
first,
event({
type: "thread.progress",
seq: 6,
runId: "run-1",
payload: { delta: "Done.", streaming: true },
}),
);
// A single tool call, not two — StrictMode's replay must not have pushed it twice.
expect(sentenceEnd?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [
{ kind: "text", text: "Let me check Done." },
{ kind: "steps", steps: [{ label: "Slack find channels", count: 1 }] },
],
}),
]);
});
it("keeps deferring a tool call across several sentence-less deltas", () => {
const initial = snapshot([]);
const afterNarration = reduceThreadSnapshot(
initial,
event({
type: "thread.progress",
seq: 4,
runId: "run-1",
payload: { text: "Let me check Slack ", streaming: true },
}),
);
const afterTool = reduceThreadSnapshot(
afterNarration,
event({
type: "agent.tool.called",
seq: 5,
runId: "run-1",
payload: { name: "SLACK_FIND_CHANNELS" },
}),
);
const afterMore = reduceThreadSnapshot(
afterTool,
event({
type: "thread.progress",
seq: 6,
runId: "run-1",
payload: { delta: "Found it, now sanding", streaming: true },
}),
);
// No sentence terminator has streamed in yet, so the tool call is still hidden and
// everything so far renders as one continuous progress block.
expect(afterMore?.messages).toEqual([
expect.objectContaining({
id: "progress:run-1",
blocks: [
{
kind: "progress",
text: "Let me check Slack Found it, now sanding",
pendingToolNames: ["SLACK_FIND_CHANNELS"],
},
],
}),
]);
});
it("restores pending tool calls from a refreshed snapshot", () => {
const refreshed = snapshot([
message("progress:run-1", [
{
kind: "progress",
text: "Let me check Slack ",
pendingToolNames: ["SLACK_FIND_CHANNELS"],
},
]),
]);
const next = reduceThreadSnapshot(
refreshed,
event({
type: "thread.progress",
seq: 6,
runId: "run-1",
payload: { delta: "now.", streaming: true },
}),
);
expect(next?.messages[0]?.blocks).toEqual([
{ kind: "text", text: "Let me check Slack now." },
{ kind: "steps", steps: [{ label: "Slack find channels", count: 1 }] },
]);
});
it("does not leak pending tool calls after clearing a thread", () => {
const withPending = snapshot([
message("progress:run-1", [
{
kind: "progress",
text: "Old unfinished narration ",
pendingToolNames: ["SLACK_FIND_CHANNELS"],
},
]),
]);
const cleared = reduceThreadSnapshot(
withPending,
event({ type: "thread.cleared", seq: 7, runId: undefined }),
);
const next = reduceThreadSnapshot(
cleared,
event({ type: "thread.progress", seq: 8, runId: "run-1", payload: { text: "Fresh." } }),
);
expect(next?.messages[0]?.blocks).toEqual([{ kind: "progress", text: "Fresh." }]);
});
it("preserves another active group run while updating live progress", () => {
const runA = {
id: "run-a",
botId: "bot-a",
threadId: "thread-1",
taskId: "task-a",
status: "running" as const,
trigger: "user" as const,
routineId: null,
modelProvider: null,
modelId: null,
error: null,
startedAt: null,
completedAt: null,
createdAt: "2026-08-16T00:00:00.000Z",
};
const otherLive = {
...message("progress:run-b", [{ kind: "progress" as const, text: "Other bot" }]),
botId: "bot-b",
runId: "run-b",
};
// The snapshot can lag behind the event stream when another group run starts.
const initial = { ...snapshot([otherLive]), activeRuns: [runA] };
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.progress",
seq: 8,
runId: "run-a",
botId: "bot-a",
payload: { text: "Current bot" },
}),
);
expect(next?.messages.map((item) => item.id)).toEqual(["progress:run-b", "progress:run-a"]);
expect(next?.messages.map((item) => item.botId)).toEqual(["bot-b", "bot-a"]);
});
it("preserves live progress from a legacy run-only snapshot", () => {
const legacyRun = threadRun("run-legacy", "bot-legacy");
const legacyLive = {
...message("progress:run-legacy", [{ kind: "progress" as const, text: "Still working" }]),
botId: legacyRun.botId,
runId: legacyRun.id,
};
const initial: ThreadSnapshot = {
...snapshot([legacyLive]),
run: legacyRun,
activeRuns: undefined,
};
const next = reduceThreadSnapshot(
initial,
event({ type: "thread.progress", runId: "run-new", payload: { text: "New work" } }),
);
expect(next?.messages.map((item) => item.id)).toEqual([
"progress:run-legacy",
"progress:run-new",
]);
});
it("preserves concurrent progress while applying a subagent update", () => {
const activeRun = threadRun("run-active");
const initial: ThreadSnapshot = {
...snapshot([
{
...message("progress:run-active", [{ kind: "progress", text: "Active" }]),
runId: "run-active",
},
{
...message("progress:run-concurrent", [{ kind: "progress", text: "Concurrent" }]),
runId: "run-concurrent",
},
]),
run: activeRun,
activeRuns: [activeRun],
};
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.subagent",
runId: "run-active",
payload: { agentId: "research", name: "Research", task: "Check", status: "running" },
}),
);
expect(next?.messages.map((item) => item.id)).toEqual([
"subagent:research",
"progress:run-active",
"progress:run-concurrent",
]);
});
it("clears the step trail once the durable answer arrives", () => {
const initial = snapshot([
message("progress:run-1", [
{ kind: "steps", steps: [{ label: "Slack find channels", count: 1 }] },
]),
]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.created",
seq: 9,
payload: { messageId: "final", role: "bot", blocks: [{ kind: "text", text: "Done" }] },
}),
);
expect(next?.messages.map((item) => item.id)).toEqual(["final"]);
});
it("updates a waiting group run without replacing the newer active run", () => {
const newerRun = {
id: "run-newer",
botId: "bot-a",
threadId: "thread-1",
taskId: "task-a",
status: "running" as const,
trigger: "user" as const,
routineId: null,
modelProvider: null,
modelId: null,
error: null,
startedAt: null,
completedAt: null,
createdAt: "2026-08-16T00:00:00.000Z",
};
const waitingRun = { ...newerRun, id: "run-waiting", botId: "bot-b", taskId: "task-b" };
const initial: ThreadSnapshot = {
...snapshot([]),
run: newerRun,
activeRuns: [newerRun, waitingRun],
};
const waiting = reduceThreadSnapshot(
initial,
event({ type: "run.waiting_input", seq: 6, runId: "run-waiting" }),
);
expect(waiting?.run).toEqual(newerRun);
expect(waiting?.activeRuns?.find((run) => run.id === "run-waiting")?.status).toBe(
"waiting_input",
);
expect(activeThreadRuns(waiting)).toEqual([
newerRun,
expect.objectContaining({ id: "run-waiting", status: "waiting_input" }),
]);
expect(activeThreadRuns({ ...initial, activeRuns: undefined })).toEqual([newerRun]);
});
it("replaces an ask message when its durable prompt state changes", () => {
const initial = snapshot([
message("ask-1", [{ kind: "ask", text: "Which city?", status: "pending" }]),
]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.updated",
seq: 7,
payload: {
messageId: "ask-1",
role: "bot",
blocks: [
{
kind: "ask",
text: "Which city?",
status: "answered",
answer: "Paris",
},
],
},
}),
);
expect(next?.messages).toHaveLength(1);
expect(next?.messages[0]?.blocks[0]).toMatchObject({ status: "answered", answer: "Paris" });
});
it("moves a task card to its new status in place", () => {
const card = {
kind: "task" as const,
taskId: "task-1",
assigneeBotId: "bot-2",
assigneeBotName: "Researcher",
title: "Owl facts",
text: "Collect owl facts.",
status: "running" as const,
};
const initial = snapshot([
message("card-1", [card]),
message("reply-1", [{ kind: "text", text: "on it" }], 4),
]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.updated",
seq: 8,
payload: { messageId: "card-1", role: "bot", blocks: [{ ...card, status: "done" }] },
}),
);
expect(next?.messages.map((entry) => entry.id)).toEqual(["card-1", "reply-1"]);
expect(next?.messages[0]?.blocks[0]).toMatchObject({ kind: "task", status: "done" });
});
it("preserves botId on durable bot messages", () => {
const initial = snapshot([]);
const next = reduceThreadSnapshot(
initial,
event({
type: "thread.message.created",
seq: 4,
botId: "bot-researcher",
payload: {
messageId: "msg-1",
role: "bot",
blocks: [{ kind: "text", text: "on it." }],
},
}),
);
expect(next?.messages[0]?.botId).toBe("bot-researcher");
});
});
describe("computer event reduction", () => {
it("applies valid lifecycle states without accepting unknown states", () => {
const initial = computer();
const running = reduceComputerStatus(
initial,
event({ type: "computer.status", payload: { status: "running" } }),
);
const unknown = reduceComputerStatus(
running,
event({ type: "computer.status", payload: { status: "destroyed" } }),
);
expect(running).toMatchObject({ state: "running", screenAvailable: true });
expect(unknown).toMatchObject({ state: "running", screenAvailable: true });
expect(unknown).toBe(running);
});
it("grants user control without overwriting the lifecycle state", () => {
const granted = reduceComputerStatus(
computer({ state: "suspended", controlHolder: "bot" }),
event({ type: "computer.takeover.granted", payload: { takeoverRequested: true } }),
);
expect(granted).toMatchObject({
state: "suspended",
controlHolder: "user",
controlBotId: "bot-1",
takeoverRequested: true,
});
expect(
reduceComputerStatus(
granted,
event({ type: "computer.takeover.granted", payload: { takeoverRequested: true } }),
),
).toBe(granted);
});
it("applies the authoritative holder when a takeover is released or expires", () => {
const initial = computer({
state: "running",
controlHolder: "user",
controlBotId: "bot-1",
takeoverRequested: true,
});
const expired = reduceComputerStatus(
initial,
event({
type: "computer.takeover.released",
payload: { holder: "none", reason: "expired" },
}),
);
const released = reduceComputerStatus(
initial,
event({
type: "computer.takeover.released",
payload: { holder: "bot", reason: "released" },
}),
);
expect(expired).toMatchObject({
state: "running",
controlHolder: "none",
controlBotId: null,
takeoverRequested: false,
});
expect(released).toMatchObject({
state: "running",
controlHolder: "bot",
controlBotId: null,
takeoverRequested: false,
});
});
it("fills in controlBotId when a grant arrives after controlHolder is already user", () => {
const granted = reduceComputerStatus(
computer({ state: "running", controlHolder: "user", controlBotId: null }),
event({ type: "computer.takeover.granted", payload: {} }),
);
expect(granted).toMatchObject({
state: "running",
controlHolder: "user",
controlBotId: "bot-1",
});
expect(userHoldsComputerControl(granted, "bot-1")).toBe(true);
expect(userHoldsComputerControl(granted, "bot-2")).toBe(false);
});
it("treats a busy bot name as a blocked takeover", () => {
expect(computerTakeoverBlocked(computer({ busyBotName: "Writer" }), "running")).toBe(true);
expect(computerTakeoverBlocked(computer({ busyBotName: "Writer" }))).toBe(false);
expect(computerTakeoverBlocked(computer({ busyBotName: null }), "running")).toBe(false);
expect(computerTakeoverBlocked(null, "running")).toBe(false);
expect(computerTakeoverBlocked(computer({ busyBotName: "Writer" }), "waiting_takeover")).toBe(
false,
);
expect(computerTakeoverBlocked(computer({ busyBotName: "Writer" }), "completed")).toBe(false);
});
it("marks takeover requested and clears control unless the lease was retained", () => {
const busy = computer({ state: "running", busyBotName: "Writer", controlHolder: "bot" });
expect(
reduceComputerStatus(busy, event({ type: "computer.takeover.requested", payload: {} })),
).toMatchObject({
busyBotName: null,
takeoverRequested: true,
controlHolder: "none",
controlBotId: null,
});
expect(
reduceComputerStatus(
computer({
state: "running",
controlHolder: "user",
controlBotId: "bot-1",
takeoverRequested: false,
}),
event({
type: "computer.takeover.requested",
payload: { retainedControl: true },
}),
),
).toMatchObject({
controlHolder: "user",
controlBotId: "bot-1",
takeoverRequested: true,
busyBotName: null,
});
expect(
reduceComputerStatus(
busy,
event({ type: "computer.takeover.granted", payload: { takeoverRequested: true } }),
),
).toMatchObject({
controlHolder: "user",
busyBotName: null,
takeoverRequested: true,
});
});
it("auto-boots stopped computers and recovers a running screen that has no URL", () => {
expect(computerPanelAutoBoot("stopped")).toBe("boot");
expect(computerPanelAutoBoot("error")).toBe("boot");
expect(computerPanelAutoBoot(undefined)).toBe("boot");
expect(computerPanelAutoBoot("running", "https://screen.example")).toBe("wait");
expect(computerPanelAutoBoot("running", null)).toBe("recover-screen");
expect(computerPanelAutoBoot("booting")).toBe("wait");
expect(computerPanelAutoBoot("suspended")).toBe("wait");
});
it("maps recover-screen to computer.boot, not computer.recover", () => {
expect(computerPanelAutoUsesBoot("recover-screen")).toBe(true);
expect(computerPanelAutoUsesBoot("boot")).toBe(true);
expect(computerPanelAutoUsesBoot("wait")).toBe(false);
});
it("shows maintenance only after a stopped or errored computer finishes booting", () => {
expect(computerPanelNeedsMaintenance("error", false)).toBe(true);
expect(computerPanelNeedsMaintenance("stopped", false)).toBe(true);
expect(computerPanelNeedsMaintenance("error", true)).toBe(false);
expect(computerPanelNeedsMaintenance("running", false)).toBe(false);
expect(computerPanelNeedsMaintenance(undefined, false)).toBe(false);
});
it("hides side-panel maintenance while the computer overlay is open", () => {
const panel = "computer";
const booting = false;
const showInSidePanel = (computerOpen: boolean) =>
panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("stopped", booting);
expect(showInSidePanel(false)).toBe(true);
expect(showInSidePanel(true)).toBe(false);
});
});
function snapshot(messages: ThreadMessage[], olderCursor: number | null = null): ThreadSnapshot {
return {
botId: "bot-1",
threadId: "thread-1",
cursor: 3,
messages,
olderCursor,
run: null,
computer: computer(),
};
}
function threadRun(id: string, botId = "bot-1"): NonNullable<ThreadSnapshot["run"]> {
return {
id,
botId,
threadId: "thread-1",
taskId: `task-${id}`,
status: "running",
trigger: "user",
routineId: null,
modelProvider: null,
modelId: null,
error: null,
startedAt: null,
completedAt: null,
createdAt: "2026-08-16T00:00:00.000Z",
};
}
function computer(overrides: Partial<ComputerStatus> = {}): ComputerStatus {
return {
botId: "bot-1",
mode: "team",
kind: "fake",
state: "booting",
controlHolder: "none",
controlBotId: null,
takeoverRequested: false,
screenAvailable: false,
screenWidth: 1280,
screenHeight: 800,
homeRevision: null,
busyBotName: null,
canUpdate: true,
...overrides,
};
}
function message(id: string, blocks: ThreadMessage["blocks"], seq = 3): ThreadMessage {
return {
id,
threadId: "thread-1",
seq,
role: "bot",
blocks,
createdAt: "2026-08-16T00:00:00.000Z",
};
}
function event(overrides: Partial<ProductEvent>): ProductEvent {
return {
id: "event-1",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
seq: 4,
type: "thread.progress",
runId: "run-1",
createdAt: "2026-08-16T00:00:01.000Z",
payload: {},
...overrides,
};
}