refactor: share session history test fixtures

This commit is contained in:
Vincent Koc
2026-06-02 02:41:09 +02:00
parent be8cb5d4ea
commit 340cc2c1e4
+93 -241
View File
@@ -5,11 +5,19 @@ import { buildSessionHistorySnapshot, SessionHistorySseState } from "./session-h
import * as sessionUtils from "./session-utils.js";
type HistorySnapshot = ReturnType<typeof buildSessionHistorySnapshot>;
type RawStateOptions = Omit<
Parameters<typeof SessionHistorySseState.fromRawSnapshot>[0],
"target" | "rawMessages"
>;
function textContent(text: string) {
return [{ type: "text" as const, text }];
}
function assistantTextMessage(text: string, seq: number) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
content: textContent(text),
__openclaw: { seq },
};
}
@@ -17,42 +25,72 @@ function assistantTextMessage(text: string, seq: number) {
function userTextMessage(text: string, seq: number) {
return {
role: "user" as const,
content: [{ type: "text" as const, text }],
content: textContent(text),
__openclaw: { seq },
};
}
function newStateWithUserText(text: string): SessionHistorySseState {
function newState(rawMessages: Array<Record<string, unknown>>, options: RawStateOptions = {}) {
return SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [userTextMessage(text, 1)],
rawMessages,
...options,
});
}
function newStateWithUserText(text: string): SessionHistorySseState {
return newState([userTextMessage(text, 1)]);
}
function expectOnlyAssistantText(snapshot: HistorySnapshot, text: string, seq: number): void {
expect(snapshot.history.messages).toEqual([assistantTextMessage(text, seq)]);
}
function messageToolCall(id: string, message: string, args: Record<string, unknown> = {}) {
return {
type: "toolCall" as const,
id,
name: "message",
arguments: {
action: "send",
message,
...args,
},
};
}
function messageToolResult(
toolCallId: string,
messageId: string,
seq?: number,
content: Record<string, unknown> = {},
) {
return {
role: "toolResult" as const,
toolName: "message",
toolCallId,
content: { ok: true, messageId, ...content },
...(seq === undefined ? {} : { __openclaw: { seq } }),
};
}
function appendAssistantText(state: SessionHistorySseState, text: string, messageSeq?: number) {
return state.appendInlineMessage({
message: {
role: "assistant",
content: textContent(text),
},
...(messageSeq === undefined ? {} : { messageSeq }),
});
}
describe("SessionHistorySseState", () => {
test("uses the initial raw snapshot for both first history and seq seeding", () => {
const readSpy = vi.spyOn(sessionUtils, "readSessionMessagesAsync").mockResolvedValue([
{
role: "assistant",
content: [{ type: "text", text: "stale disk message" }],
__openclaw: { seq: 1 },
},
]);
const readSpy = vi
.spyOn(sessionUtils, "readSessionMessagesAsync")
.mockResolvedValue([assistantTextMessage("stale disk message", 1)]);
try {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "fresh snapshot message" }],
__openclaw: { seq: 2 },
},
],
});
const state = newState([assistantTextMessage("fresh snapshot message", 2)]);
expect(state.snapshot().messages).toHaveLength(1);
expect(
@@ -97,24 +135,9 @@ describe("SessionHistorySseState", () => {
});
test("uses carried sequence for inline SSE appends", () => {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "initial" }],
__openclaw: { seq: 2 },
},
],
});
const state = newState([assistantTextMessage("initial", 2)]);
const appended = state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "carried" }],
},
messageSeq: 9,
});
const appended = appendAssistantText(state, "carried", 9);
expect(appended?.messageSeq).toBe(9);
expect(state.snapshot().messages.at(-1)?.["__openclaw"]?.seq).toBe(9);
@@ -128,16 +151,9 @@ describe("SessionHistorySseState", () => {
message: {
role: "assistant",
content: [
{
type: "toolCall",
id: "call-message-channel-hint",
name: "message",
arguments: {
action: "send",
channel: "telegram",
message: "Still the current chat.",
},
},
messageToolCall("call-message-channel-hint", "Still the current chat.", {
channel: "telegram",
}),
],
},
messageSeq: 2,
@@ -145,23 +161,14 @@ describe("SessionHistorySseState", () => {
).toBe(2);
expect(
state.appendInlineMessage({
message: {
role: "toolResult",
toolName: "message",
toolCallId: "call-message-channel-hint",
content: { ok: true, messageId: "24270", chatId: "current-run" },
},
message: messageToolResult("call-message-channel-hint", "24270", undefined, {
chatId: "current-run",
}),
messageSeq: 3,
})?.messageSeq,
).toBe(3);
const appended = state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
},
messageSeq: 4,
});
const appended = appendAssistantText(state, "NO_REPLY", 4);
expect(appended?.messageSeq).toBe(4);
expect(
@@ -183,38 +190,14 @@ describe("SessionHistorySseState", () => {
test("keeps cursors when a paginated history page starts with a message-tool mirror", () => {
const snapshot = buildSessionHistorySnapshot({
rawMessages: [
{
role: "user",
content: [{ type: "text", text: "reply here" }],
__openclaw: { seq: 1 },
},
userTextMessage("reply here", 1),
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call-message-cursor",
name: "message",
arguments: {
action: "send",
message: "Cursor-visible reply.",
},
},
],
content: [messageToolCall("call-message-cursor", "Cursor-visible reply.")],
__openclaw: { seq: 2 },
},
{
role: "toolResult",
toolName: "message",
toolCallId: "call-message-cursor",
content: { ok: true, messageId: "cursor" },
__openclaw: { seq: 3 },
},
{
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
__openclaw: { seq: 4 },
},
messageToolResult("call-message-cursor", "cursor", 3),
assistantTextMessage("NO_REPLY", 4),
],
limit: 1,
});
@@ -236,69 +219,28 @@ describe("SessionHistorySseState", () => {
});
test("requests refresh when silent control reply completes multiple message-tool mirrors", () => {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "user",
content: [{ type: "text", text: "send both here" }],
__openclaw: { seq: 1 },
},
],
});
const state = newState([userTextMessage("send both here", 1)]);
state.appendInlineMessage({
message: {
role: "assistant",
content: [
{
type: "toolCall",
id: "call-message-first",
name: "message",
arguments: {
action: "send",
message: "First visible reply.",
},
},
{
type: "toolCall",
id: "call-message-second",
name: "message",
arguments: {
action: "send",
message: "Second visible reply.",
},
},
messageToolCall("call-message-first", "First visible reply."),
messageToolCall("call-message-second", "Second visible reply."),
],
},
messageSeq: 2,
});
state.appendInlineMessage({
message: {
role: "toolResult",
toolName: "message",
toolCallId: "call-message-first",
content: { ok: true, messageId: "first" },
},
message: messageToolResult("call-message-first", "first"),
messageSeq: 3,
});
state.appendInlineMessage({
message: {
role: "toolResult",
toolName: "message",
toolCallId: "call-message-second",
content: { ok: true, messageId: "second" },
},
message: messageToolResult("call-message-second", "second"),
messageSeq: 4,
});
const appended = state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
},
messageSeq: 5,
});
const appended = appendAssistantText(state, "NO_REPLY", 5);
expect(appended).toEqual({ shouldRefresh: true });
expect(
@@ -314,13 +256,7 @@ describe("SessionHistorySseState", () => {
test("does not emit a no-op hidden inline control reply", () => {
const state = newStateWithUserText("reply here");
const appended = state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
},
messageSeq: 2,
});
const appended = appendAssistantText(state, "NO_REPLY", 2);
expect(appended).toBeNull();
expect(state.snapshot().messages).toHaveLength(1);
@@ -329,16 +265,7 @@ describe("SessionHistorySseState", () => {
test("requests refresh when inline TTS supplement merges into an existing assistant message", () => {
const visibleText = "Here is the answer.";
const textSha256 = createHash("sha256").update(visibleText).digest("hex");
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: visibleText }],
__openclaw: { seq: 2 },
},
],
});
const state = newState([assistantTextMessage(visibleText, 2)]);
const appended = state.appendInlineMessage({
message: {
@@ -365,7 +292,7 @@ describe("SessionHistorySseState", () => {
{
role: "assistant",
content: [
{ type: "text", text: visibleText },
textContent(visibleText)[0],
{
type: "attachment",
attachment: {
@@ -382,24 +309,9 @@ describe("SessionHistorySseState", () => {
});
test("requests refresh for non-monotonic carried inline sequence", () => {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "current" }],
__openclaw: { seq: 5 },
},
],
});
const state = newState([assistantTextMessage("current", 5)]);
const appended = state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "rewound branch" }],
},
messageSeq: 3,
});
const appended = appendAssistantText(state, "rewound branch", 3);
expect(appended).toEqual({ shouldRefresh: true });
expect(state.snapshot().messages).toHaveLength(1);
@@ -408,13 +320,7 @@ describe("SessionHistorySseState", () => {
test("marks bounded tail snapshots as having older history", () => {
const snapshot = buildSessionHistorySnapshot({
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "tail" }],
__openclaw: { seq: 99 },
},
],
rawMessages: [assistantTextMessage("tail", 99)],
limit: 1,
rawTranscriptSeq: 99,
totalRawMessages: 99,
@@ -430,25 +336,11 @@ describe("SessionHistorySseState", () => {
const tailReadSpy = vi
.spyOn(sessionUtils, "readRecentSessionMessagesWithStatsAsync")
.mockResolvedValueOnce({
messages: [
{
role: "assistant",
content: [{ type: "text", text: "tail two" }],
__openclaw: { seq: 8 },
},
],
messages: [assistantTextMessage("tail two", 8)],
totalMessages: 8,
});
try {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "tail one" }],
__openclaw: { seq: 7 },
},
],
const state = newState([assistantTextMessage("tail one", 7)], {
rawTranscriptSeq: 7,
totalRawMessages: 7,
limit: 1,
@@ -566,21 +458,11 @@ describe("SessionHistorySseState", () => {
},
__openclaw: { seq: 1 },
},
{
role: "assistant",
content: [{ type: "text", text: "clean child result" }],
__openclaw: { seq: 2 },
},
assistantTextMessage("clean child result", 2),
],
});
expect(snapshot.history.messages).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "clean child result" }],
__openclaw: { seq: 2 },
},
]);
expectOnlyAssistantText(snapshot, "clean child result", 2);
});
test("hides heartbeat prompt and ok acknowledgements from visible history", () => {
@@ -591,45 +473,22 @@ describe("SessionHistorySseState", () => {
content: `${HEARTBEAT_PROMPT}\nWhen reading HEARTBEAT.md, use workspace file /tmp/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.`,
__openclaw: { seq: 1 },
},
{
role: "assistant",
content: [{ type: "text", text: "HEARTBEAT_OK" }],
__openclaw: { seq: 2 },
},
assistantTextMessage("HEARTBEAT_OK", 2),
{
role: "user",
content: HEARTBEAT_PROMPT,
__openclaw: { seq: 3 },
},
{
role: "assistant",
content: [{ type: "text", text: "Disk usage crossed 95 percent." }],
__openclaw: { seq: 4 },
},
assistantTextMessage("Disk usage crossed 95 percent.", 4),
],
});
expect(snapshot.history.messages).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "Disk usage crossed 95 percent." }],
__openclaw: { seq: 4 },
},
]);
expectOnlyAssistantText(snapshot, "Disk usage crossed 95 percent.", 4);
expect(snapshot.rawTranscriptSeq).toBe(4);
});
test("does not append heartbeat or internal-only SSE messages", () => {
const state = SessionHistorySseState.fromRawSnapshot({
target: { sessionId: "sess-main" },
rawMessages: [
{
role: "assistant",
content: [{ type: "text", text: "already visible" }],
__openclaw: { seq: 1 },
},
],
});
const state = newState([assistantTextMessage("already visible", 1)]);
expect(
state.appendInlineMessage({
@@ -639,14 +498,7 @@ describe("SessionHistorySseState", () => {
},
}),
).toBeNull();
expect(
state.appendInlineMessage({
message: {
role: "assistant",
content: [{ type: "text", text: "HEARTBEAT_OK" }],
},
}),
).toBeNull();
expect(appendAssistantText(state, "HEARTBEAT_OK")).toBeNull();
expect(
state.appendInlineMessage({
message: {