refactor(tests): dedupe gateway chat history fixtures

This commit is contained in:
Peter Steinberger
2026-03-03 02:00:43 +00:00
parent 25a2fe2bea
commit 5c18ba6f65

View File

@@ -42,6 +42,105 @@ async function waitFor(condition: () => boolean, timeoutMs = 250) {
}
describe("gateway server chat", () => {
const buildNoReplyHistoryFixture = (includeMixedAssistant = false) => [
{
role: "user",
content: [{ type: "text", text: "hello" }],
timestamp: 1,
},
{
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 2,
},
{
role: "assistant",
content: [{ type: "text", text: "real reply" }],
timestamp: 3,
},
{
role: "assistant",
text: "real text field reply",
content: "NO_REPLY",
timestamp: 4,
},
{
role: "user",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 5,
},
...(includeMixedAssistant
? [
{
role: "assistant",
content: [
{ type: "text", text: "NO_REPLY" },
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
],
timestamp: 6,
},
]
: []),
];
const loadChatHistoryWithMessages = async (
messages: Array<Record<string, unknown>>,
): Promise<unknown[]> => {
return withMainSessionStore(async (dir) => {
const lines = messages.map((message) => JSON.stringify({ message }));
await fs.writeFile(path.join(dir, "sess-main.jsonl"), lines.join("\n"), "utf-8");
const res = await rpcReq<{ messages?: unknown[] }>(ws, "chat.history", {
sessionKey: "main",
});
expect(res.ok).toBe(true);
return res.payload?.messages ?? [];
});
};
const withMainSessionStore = async <T>(run: (dir: string) => Promise<T>): Promise<T> => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
try {
testState.sessionStorePath = path.join(dir, "sessions.json");
await writeSessionStore({
entries: {
main: {
sessionId: "sess-main",
updatedAt: Date.now(),
},
},
});
return await run(dir);
} finally {
testState.sessionStorePath = undefined;
await fs.rm(dir, { recursive: true, force: true });
}
};
const collectHistoryTextValues = (historyMessages: unknown[]) =>
historyMessages
.map((message) => {
if (message && typeof message === "object") {
const entry = message as { text?: unknown };
if (typeof entry.text === "string") {
return entry.text;
}
}
return extractFirstTextBlock(message);
})
.filter((value): value is string => typeof value === "string");
const expectAgentWaitTimeout = (res: Awaited<ReturnType<typeof rpcReq>>) => {
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("timeout");
};
const expectAgentWaitStartedAt = (res: Awaited<ReturnType<typeof rpcReq>>, startedAt: number) => {
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("ok");
expect(res.payload?.startedAt).toBe(startedAt);
};
test("sanitizes inbound chat.send message text and rejects null bytes", async () => {
const nullByteRes = await rpcReq(ws, "chat.send", {
sessionKey: "main",
@@ -305,89 +404,17 @@ describe("gateway server chat", () => {
});
test("chat.history hides assistant NO_REPLY-only entries", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
try {
testState.sessionStorePath = path.join(dir, "sessions.json");
await writeSessionStore({
entries: {
main: {
sessionId: "sess-main",
updatedAt: Date.now(),
},
},
});
const messages = [
{
role: "user",
content: [{ type: "text", text: "hello" }],
timestamp: 1,
},
{
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 2,
},
{
role: "assistant",
content: [{ type: "text", text: "real reply" }],
timestamp: 3,
},
{
role: "assistant",
text: "real text field reply",
content: "NO_REPLY",
timestamp: 4,
},
{
role: "user",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 5,
},
];
const lines = messages.map((message) => JSON.stringify({ message }));
await fs.writeFile(path.join(dir, "sess-main.jsonl"), lines.join("\n"), "utf-8");
const res = await rpcReq<{ messages?: unknown[] }>(ws, "chat.history", {
sessionKey: "main",
});
expect(res.ok).toBe(true);
const historyMessages = res.payload?.messages ?? [];
const textValues = historyMessages
.map((message) => {
if (message && typeof message === "object") {
const entry = message as { text?: unknown };
if (typeof entry.text === "string") {
return entry.text;
}
}
return extractFirstTextBlock(message);
})
.filter((value): value is string => typeof value === "string");
// The NO_REPLY assistant message (content block) should be dropped.
// The assistant with text="real text field reply" + content="NO_REPLY" stays
// because entry.text takes precedence over entry.content for the silent check.
// The user message with NO_REPLY text is preserved (only assistant filtered).
expect(textValues).toEqual(["hello", "real reply", "real text field reply", "NO_REPLY"]);
} finally {
testState.sessionStorePath = undefined;
await fs.rm(dir, { recursive: true, force: true });
}
const historyMessages = await loadChatHistoryWithMessages(buildNoReplyHistoryFixture());
const textValues = collectHistoryTextValues(historyMessages);
// The NO_REPLY assistant message (content block) should be dropped.
// The assistant with text="real text field reply" + content="NO_REPLY" stays
// because entry.text takes precedence over entry.content for the silent check.
// The user message with NO_REPLY text is preserved (only assistant filtered).
expect(textValues).toEqual(["hello", "real reply", "real text field reply", "NO_REPLY"]);
});
test("routes chat.send slash commands without agent runs", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
try {
testState.sessionStorePath = path.join(dir, "sessions.json");
await writeSessionStore({
entries: {
main: {
sessionId: "sess-main",
updatedAt: Date.now(),
},
},
});
await withMainSessionStore(async () => {
const spy = vi.mocked(agentCommand);
const callsBefore = spy.mock.calls.length;
const eventPromise = onceMessage(
@@ -407,98 +434,36 @@ describe("gateway server chat", () => {
expect(res.ok).toBe(true);
await eventPromise;
expect(spy.mock.calls.length).toBe(callsBefore);
} finally {
testState.sessionStorePath = undefined;
await fs.rm(dir, { recursive: true, force: true });
}
});
});
test("chat.history hides assistant NO_REPLY-only entries and keeps mixed-content assistant entries", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
try {
testState.sessionStorePath = path.join(dir, "sessions.json");
await writeSessionStore({
entries: {
main: {
sessionId: "sess-main",
updatedAt: Date.now(),
},
},
});
const historyMessages = await loadChatHistoryWithMessages(buildNoReplyHistoryFixture(true));
const roleAndText = historyMessages
.map((message) => {
const role =
message &&
typeof message === "object" &&
typeof (message as { role?: unknown }).role === "string"
? (message as { role: string }).role
: "unknown";
const text =
message &&
typeof message === "object" &&
typeof (message as { text?: unknown }).text === "string"
? (message as { text: string }).text
: (extractFirstTextBlock(message) ?? "");
return `${role}:${text}`;
})
.filter((entry) => entry !== "unknown:");
const messages = [
{
role: "user",
content: [{ type: "text", text: "hello" }],
timestamp: 1,
},
{
role: "assistant",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 2,
},
{
role: "assistant",
content: [{ type: "text", text: "real reply" }],
timestamp: 3,
},
{
role: "assistant",
text: "real text field reply",
content: "NO_REPLY",
timestamp: 4,
},
{
role: "user",
content: [{ type: "text", text: "NO_REPLY" }],
timestamp: 5,
},
{
role: "assistant",
content: [
{ type: "text", text: "NO_REPLY" },
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
],
timestamp: 6,
},
];
const lines = messages.map((message) => JSON.stringify({ message }));
await fs.writeFile(path.join(dir, "sess-main.jsonl"), lines.join("\n"), "utf-8");
const res = await rpcReq<{ messages?: unknown[] }>(ws, "chat.history", {
sessionKey: "main",
});
expect(res.ok).toBe(true);
const historyMessages = res.payload?.messages ?? [];
const roleAndText = historyMessages
.map((message) => {
const role =
message &&
typeof message === "object" &&
typeof (message as { role?: unknown }).role === "string"
? (message as { role: string }).role
: "unknown";
const text =
message &&
typeof message === "object" &&
typeof (message as { text?: unknown }).text === "string"
? (message as { text: string }).text
: (extractFirstTextBlock(message) ?? "");
return `${role}:${text}`;
})
.filter((entry) => entry !== "unknown:");
expect(roleAndText).toEqual([
"user:hello",
"assistant:real reply",
"assistant:real text field reply",
"user:NO_REPLY",
"assistant:NO_REPLY",
]);
} finally {
testState.sessionStorePath = undefined;
await fs.rm(dir, { recursive: true, force: true });
}
expect(roleAndText).toEqual([
"user:hello",
"assistant:real reply",
"assistant:real text field reply",
"user:NO_REPLY",
"assistant:NO_REPLY",
]);
});
test("agent events include sessionKey and agent.wait covers lifecycle flows", async () => {
@@ -568,9 +533,7 @@ describe("gateway server chat", () => {
});
const res = await waitP;
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("ok");
expect(res.payload?.startedAt).toBe(200);
expectAgentWaitStartedAt(res, 200);
}
{
@@ -594,8 +557,7 @@ describe("gateway server chat", () => {
runId: "run-wait-3",
timeoutMs: 30,
});
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("timeout");
expectAgentWaitTimeout(res);
}
{
@@ -613,8 +575,7 @@ describe("gateway server chat", () => {
});
const res = await waitP;
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("timeout");
expectAgentWaitTimeout(res);
}
{
@@ -638,9 +599,7 @@ describe("gateway server chat", () => {
});
const res = await waitP;
expect(res.ok).toBe(true);
expect(res.payload?.status).toBe("ok");
expect(res.payload?.startedAt).toBe(123);
expectAgentWaitStartedAt(res, 123);
expect(res.payload?.endedAt).toBe(456);
}
} finally {