test: cover durable compaction activity

This commit is contained in:
Shakker
2026-08-21 16:03:06 +01:00
parent a5bac03e51
commit 7b5430a2d3
4 changed files with 123 additions and 4 deletions
@@ -0,0 +1,70 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { persistCodexContextCompactionActivity } from "./context-compaction-activity.js";
const appendMessage = vi.hoisted(() => vi.fn());
const publishUpdate = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/session-transcript-runtime", () => ({
appendSessionTranscriptMessageByIdentity: appendMessage,
publishSessionTranscriptUpdateByIdentity: publishUpdate,
}));
beforeEach(() => {
appendMessage.mockReset();
publishUpdate.mockReset();
});
describe("persistCodexContextCompactionActivity", () => {
it("publishes one model-excluded activity and leaves replay deduplication to transcript identity", async () => {
appendMessage
.mockImplementationOnce(async (params: { message: unknown }) => ({
appended: true,
message: params.message,
messageId: "activity-message",
}))
.mockResolvedValueOnce({
appended: false,
message: {},
messageId: "activity-message",
});
const params = {
run: {
runId: "run-1",
workspaceDir: "/workspace",
sessionTarget: {
agentId: "main",
sessionId: "session-1",
sessionKey: "agent:main:dashboard:session-1",
storePath: "/state/openclaw-agent.sqlite",
},
},
threadId: "thread-1",
turnId: "turn-1",
itemId: "compact-1",
timestamp: 123,
} as Parameters<typeof persistCodexContextCompactionActivity>[0];
await persistCodexContextCompactionActivity(params);
await persistCodexContextCompactionActivity(params);
expect(appendMessage).toHaveBeenCalledTimes(2);
expect(appendMessage.mock.calls[0]?.[0]).toMatchObject({
eventId: "codex-context-compaction:thread-1:turn-1:compact-1",
message: {
role: "custom",
customType: "openclaw.context-compaction",
content: "Context compacted",
display: true,
excludeFromContext: true,
idempotencyKey: "codex-context-compaction:thread-1:turn-1:compact-1",
},
});
expect(publishUpdate).toHaveBeenCalledOnce();
expect(publishUpdate.mock.calls[0]?.[0]).toMatchObject({
update: {
messageId: "activity-message",
runId: "run-1",
},
});
});
});
@@ -133,6 +133,19 @@ function toolResultEntry(
}
describe("buildSessionContext", () => {
it("keeps display-only custom activity out of model input", () => {
const activity = {
role: "custom" as const,
customType: "openclaw.context-compaction",
content: "Context compacted",
display: true,
excludeFromContext: true,
timestamp: Date.parse(timestamp),
};
expect(convertToLlm([activity])).toEqual([]);
});
it("keeps private shell executions in history without projecting them into context", () => {
const hiddenEntry = bashEntry("hidden", "initial", "private shell output", true);
const visibleEntry = bashEntry("visible", "hidden", "visible shell output", false);
+11 -4
View File
@@ -86,12 +86,18 @@ export function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup>
currentGroup?.role === "assistant" &&
isKeyedAssistantStreamFallbackMessage(currentGroup.messages[0]?.message) !==
isKeyedAssistantStreamFallbackMessage(item.message);
const splitsRuntimeActivity =
role === "assistant" &&
currentGroup?.role === "assistant" &&
isContextCompactionActivity(currentGroup.messages[0]?.message) !==
isContextCompactionActivity(item.message);
if (
!currentGroup ||
startsProjectedTurn ||
currentGroup.role !== role ||
splitsAssistantCommentary ||
splitsRuntimeActivity ||
(shouldSplitBySender &&
(currentGroup.senderLabel !== senderLabel ||
senderIdentityKey(currentGroup.sender) !== senderIdentityKey(sender)))
@@ -542,14 +548,15 @@ export function assistantGroupCanOwnActiveRunStatus(group: MessageGroup): boolea
return (
group.role.toLowerCase() === "assistant" &&
!assistantGroupIsForwardedBoundary(group) &&
!group.messages.every(
({ message }) =>
asRecord(asRecord(message)?.["__openclaw"])?.runtimeActivityKind === "context_compaction",
) &&
!group.messages.every(({ message }) => isContextCompactionActivity(message)) &&
groupHasVisibleReplyContent(group)
);
}
function isContextCompactionActivity(message: unknown): boolean {
return asRecord(asRecord(message)?.["__openclaw"])?.runtimeActivityKind === "context_compaction";
}
// History carries no final-vs-commentary marker (commentary exists only as
// live stream segments), so the last assistant group with visible content
// stands in for the final reply. Turns whose last content is commentary
+29
View File
@@ -757,6 +757,35 @@ describe("collapseCompletedTurnWork", () => {
expect(requireGroup(items[2]).role).toBe("assistant");
});
it("keeps durable context compaction inside completed work instead of treating it as the reply", () => {
const items = collapsedItems({
messages: [
userMessage("do it", 1_000),
{
role: "custom",
customType: "openclaw.context-compaction",
content: "Context compacted",
display: true,
excludeFromContext: true,
timestamp: 2_000,
},
assistantMessage("All done.", 3_000),
],
});
expect(items.map((item) => item.kind)).toEqual(["group", "work-group", "group"]);
const work = requireWorkGroup(items[1]);
expect(work.groups).toHaveLength(1);
expect(work.groups[0]?.messages[0]?.message).toMatchObject({
role: "assistant",
content: [{ type: "text", text: "Context compacted" }],
__openclaw: { runtimeActivityKind: "context_compaction" },
});
expect(requireGroup(items[2]).messages[0]?.message).toMatchObject({
content: "All done.",
});
});
it.each([
{
role: "assistant",