First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user