test: split compact command lifecycle coverage (#123765)

This commit is contained in:
Peter Steinberger
2026-08-14 10:43:29 -07:00
committed by GitHub
parent e03d1a42f8
commit 0cd18e1fb9
3 changed files with 328 additions and 285 deletions
@@ -0,0 +1,213 @@
// Tests compact-command session authority across awaited lifecycle transitions.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
abortEmbeddedAgentRun,
buildCompactParams,
compactEmbeddedAgentSession,
handleCompactCommand,
incrementCompactionCount,
isCurrentSessionEntry,
isEmbeddedAgentRunAbortableForCompaction,
resetCompactCommandMocks,
waitForEmbeddedAgentRunEnd,
} from "./commands-compact.test-support.js";
import type { HandleCommandsParams } from "./commands-types.js";
describe("handleCompactCommand lifecycle authority", () => {
beforeEach(resetCompactCommandMocks);
it("does not abort the command reply run before compacting", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(false);
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: false,
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.shouldContinue).toBe(false);
expect(vi.mocked(isEmbeddedAgentRunAbortableForCompaction)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(abortEmbeddedAgentRun)).not.toHaveBeenCalled();
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).not.toHaveBeenCalled();
expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce();
});
it("does not abort a run after the bound session changes", async () => {
vi.mocked(isCurrentSessionEntry).mockReturnValueOnce(false);
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toEqual({
compacted: false,
reason: "command session changed",
});
expect(vi.mocked(isEmbeddedAgentRunAbortableForCompaction)).not.toHaveBeenCalled();
expect(vi.mocked(abortEmbeddedAgentRun)).not.toHaveBeenCalled();
expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled();
});
it("waits for an active embedded run before compacting even when abort is rejected", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
vi.mocked(abortEmbeddedAgentRun).mockReturnValueOnce(false);
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: false,
});
await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(vi.mocked(abortEmbeddedAgentRun)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).toHaveBeenCalledWith("session-1", 15_000);
expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce();
});
it("does not replace an active run when abort drain times out", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
vi.mocked(waitForEmbeddedAgentRunEnd).mockResolvedValueOnce(false);
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result).toEqual({
shouldContinue: false,
sessionCompaction: {
compacted: false,
reason: "the previous run is still stopping",
},
reply: {
text: "⚙️ Compaction unavailable: the previous run is still stopping.",
isStatusNotice: true,
},
});
expect(vi.mocked(abortEmbeddedAgentRun)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).toHaveBeenCalledWith("session-1", 15_000);
expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled();
});
it("accepts the successor session produced by context-engine accounting", async () => {
let currentSessionId = "native-session";
vi.mocked(isCurrentSessionEntry).mockImplementation(
({ expected }) => expected.sessionId === currentSessionId,
);
vi.mocked(incrementCompactionCount).mockImplementationOnce(async () => {
currentSessionId = "successor-session";
return 1;
});
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: true,
compactionKind: "context-engine",
result: {
summary: "compacted",
firstKeptEntryId: "first-kept",
sessionId: "successor-session",
tokensBefore: 999,
tokensAfter: 321,
},
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "native-session",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toMatchObject({ compacted: true, tokensAfter: 321 });
});
it("rejects a successor session when compaction accounting did not commit", async () => {
let currentSessionId = "native-session";
vi.mocked(isCurrentSessionEntry).mockImplementation(
({ expected }) => expected.sessionId === currentSessionId,
);
vi.mocked(incrementCompactionCount).mockImplementationOnce(async () => {
currentSessionId = "successor-session";
return undefined;
});
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: true,
compactionKind: "context-engine",
result: {
summary: "compacted",
firstKeptEntryId: "first-kept",
sessionId: "successor-session",
tokensBefore: 999,
tokensAfter: 321,
},
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: { sessionId: "native-session", updatedAt: Date.now() },
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toEqual({
compacted: false,
reason: "command session changed",
});
});
});
@@ -0,0 +1,104 @@
// Shared compact-command mocks and fixtures for focused behavior suites.
import { vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentDirMock,
resolveSessionAgentIdMock,
} from "./commands-agent-scope.test-support.js";
import type { HandleCommandsParams } from "./commands-types.js";
vi.mock("./commands-compact.runtime.js", () => ({
abortEmbeddedAgentRun: vi.fn(),
compactEmbeddedAgentSession: vi.fn(),
enqueueSystemEvent: vi.fn(),
formatContextUsageShort: vi.fn(() => "Context 12.1k"),
formatTokenCount: vi.fn((value: number) => `${value}`),
incrementCompactionCount: vi.fn(),
isCurrentSessionEntry: vi.fn(() => true),
isEmbeddedAgentRunAbortableForCompaction: vi.fn().mockReturnValue(false),
resolveFreshSessionTotalTokens: vi.fn(() => 12_345),
resolveSessionFilePathOptions: vi.fn(() => ({})),
waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(true),
}));
export const {
abortEmbeddedAgentRun,
compactEmbeddedAgentSession,
formatContextUsageShort,
incrementCompactionCount,
isCurrentSessionEntry,
isEmbeddedAgentRunAbortableForCompaction,
waitForEmbeddedAgentRunEnd,
} = await import("./commands-compact.runtime.js");
export const { handleCompactCommand } = await import("./commands-compact.js");
export function buildCompactParams(
commandBodyNormalized: string,
cfg: OpenClawConfig,
): HandleCommandsParams {
return {
cfg,
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
CommandBody: commandBodyNormalized,
commandText: commandBodyNormalized,
},
command: {
commandBodyNormalized,
isAuthorizedSender: true,
senderIsOwner: false,
senderId: "owner",
channel: "whatsapp",
ownerList: [],
},
sessionKey: "agent:main:main",
sessionStore: {},
resolveDefaultThinkingLevel: async () => "medium",
} as unknown as HandleCommandsParams;
}
export function resetCompactCommandMocks() {
vi.clearAllMocks();
vi.mocked(incrementCompactionCount).mockResolvedValue(1);
vi.mocked(isCurrentSessionEntry).mockReturnValue(true);
resolveAgentDirMock.mockImplementation(
(_cfg: unknown, agentId: string) => `/tmp/workspace/.openclaw/agents/${agentId}/agent`,
);
resolveSessionAgentIdMock.mockReturnValue("main");
}
export function requireCompactEmbeddedAgentSessionCall(index = 0) {
const call = vi.mocked(compactEmbeddedAgentSession).mock.calls[index]?.[0];
if (!call) {
throw new Error(`compactEmbeddedAgentSession call ${index} missing`);
}
return call;
}
export function requireIncrementCompactionCountCall(index = 0) {
const call = vi.mocked(incrementCompactionCount).mock.calls[index]?.[0];
if (!call) {
throw new Error(`incrementCompactionCount call ${index} missing`);
}
return call;
}
export function requireResolveSessionAgentIdCall(index = 0) {
const call = (
resolveSessionAgentIdMock.mock.calls[index] as unknown as [unknown] | undefined
)?.[0] as { sessionKey?: string; config?: OpenClawConfig } | undefined;
if (!call) {
throw new Error(`resolveSessionAgentId call ${index} missing`);
}
return call;
}
export function requireResolveAgentDirCall(index = 0) {
const call = resolveAgentDirMock.mock.calls[index] as [OpenClawConfig, string] | undefined;
if (!call) {
throw new Error(`resolveAgentDir call ${index} missing`);
}
return call;
}
+11 -285
View File
@@ -6,104 +6,24 @@ import {
resolveAgentDirMock,
resolveSessionAgentIdMock,
} from "./commands-agent-scope.test-support.js";
import type { HandleCommandsParams } from "./commands-types.js";
vi.mock("./commands-compact.runtime.js", () => ({
abortEmbeddedAgentRun: vi.fn(),
compactEmbeddedAgentSession: vi.fn(),
enqueueSystemEvent: vi.fn(),
formatContextUsageShort: vi.fn(() => "Context 12.1k"),
formatTokenCount: vi.fn((value: number) => `${value}`),
incrementCompactionCount: vi.fn(),
isCurrentSessionEntry: vi.fn(() => true),
isEmbeddedAgentRunAbortableForCompaction: vi.fn().mockReturnValue(false),
resolveFreshSessionTotalTokens: vi.fn(() => 12_345),
resolveSessionFilePathOptions: vi.fn(() => ({})),
waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(true),
}));
const {
import {
abortEmbeddedAgentRun,
buildCompactParams,
compactEmbeddedAgentSession,
formatContextUsageShort,
handleCompactCommand,
incrementCompactionCount,
isCurrentSessionEntry,
isEmbeddedAgentRunAbortableForCompaction,
requireCompactEmbeddedAgentSessionCall,
requireIncrementCompactionCountCall,
requireResolveAgentDirCall,
requireResolveSessionAgentIdCall,
resetCompactCommandMocks,
waitForEmbeddedAgentRunEnd,
} = await import("./commands-compact.runtime.js");
const { handleCompactCommand } = await import("./commands-compact.js");
function buildCompactParams(
commandBodyNormalized: string,
cfg: OpenClawConfig,
): HandleCommandsParams {
return {
cfg,
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
CommandBody: commandBodyNormalized,
commandText: commandBodyNormalized,
},
command: {
commandBodyNormalized,
isAuthorizedSender: true,
senderIsOwner: false,
senderId: "owner",
channel: "whatsapp",
ownerList: [],
},
sessionKey: "agent:main:main",
sessionStore: {},
resolveDefaultThinkingLevel: async () => "medium",
} as unknown as HandleCommandsParams;
}
function requireCompactEmbeddedAgentSessionCall(index = 0) {
const call = vi.mocked(compactEmbeddedAgentSession).mock.calls[index]?.[0];
if (!call) {
throw new Error(`compactEmbeddedAgentSession call ${index} missing`);
}
return call;
}
function requireResolveSessionAgentIdCall(index = 0) {
const call = (
resolveSessionAgentIdMock.mock.calls[index] as unknown as [unknown] | undefined
)?.[0] as { sessionKey?: string; config?: OpenClawConfig } | undefined;
if (!call) {
throw new Error(`resolveSessionAgentId call ${index} missing`);
}
return call;
}
function requireResolveAgentDirCall(index = 0) {
const call = resolveAgentDirMock.mock.calls[index] as [OpenClawConfig, string] | undefined;
if (!call) {
throw new Error(`resolveAgentDir call ${index} missing`);
}
return call;
}
function requireIncrementCompactionCountCall(index = 0) {
const call = vi.mocked(incrementCompactionCount).mock.calls[index]?.[0];
if (!call) {
throw new Error(`incrementCompactionCount call ${index} missing`);
}
return call;
}
} from "./commands-compact.test-support.js";
import type { HandleCommandsParams } from "./commands-types.js";
describe("handleCompactCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(incrementCompactionCount).mockResolvedValue(1);
vi.mocked(isCurrentSessionEntry).mockReturnValue(true);
resolveAgentDirMock.mockImplementation(
(_cfg: unknown, agentId: string) => `/tmp/workspace/.openclaw/agents/${agentId}/agent`,
);
resolveSessionAgentIdMock.mockReturnValue("main");
});
beforeEach(resetCompactCommandMocks);
it("returns null when command is not /compact", async () => {
const result = await handleCompactCommand(
@@ -237,122 +157,6 @@ describe("handleCompactCommand", () => {
expect(params.command.ownerList).toEqual(ownerIds);
});
it("does not abort the command reply run before compacting", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(false);
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: false,
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.shouldContinue).toBe(false);
expect(vi.mocked(isEmbeddedAgentRunAbortableForCompaction)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(abortEmbeddedAgentRun)).not.toHaveBeenCalled();
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).not.toHaveBeenCalled();
expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce();
});
it("does not abort a run after the bound session changes", async () => {
vi.mocked(isCurrentSessionEntry).mockReturnValueOnce(false);
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toEqual({
compacted: false,
reason: "command session changed",
});
expect(vi.mocked(isEmbeddedAgentRunAbortableForCompaction)).not.toHaveBeenCalled();
expect(vi.mocked(abortEmbeddedAgentRun)).not.toHaveBeenCalled();
expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled();
});
it("waits for an active embedded run before compacting even when abort is rejected", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
vi.mocked(abortEmbeddedAgentRun).mockReturnValueOnce(false);
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: false,
});
await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(vi.mocked(abortEmbeddedAgentRun)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).toHaveBeenCalledWith("session-1", 15_000);
expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce();
});
it("does not replace an active run when abort drain times out", async () => {
vi.mocked(isEmbeddedAgentRunAbortableForCompaction).mockReturnValueOnce(true);
vi.mocked(waitForEmbeddedAgentRunEnd).mockResolvedValueOnce(false);
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result).toEqual({
shouldContinue: false,
sessionCompaction: {
compacted: false,
reason: "the previous run is still stopping",
},
reply: {
text: "⚙️ Compaction unavailable: the previous run is still stopping.",
isStatusNotice: true,
},
});
expect(vi.mocked(abortEmbeddedAgentRun)).toHaveBeenCalledWith("session-1");
expect(vi.mocked(waitForEmbeddedAgentRunEnd)).toHaveBeenCalledWith("session-1", 15_000);
expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled();
});
it("treats already-under-target manual compaction as skipped", async () => {
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: false,
@@ -863,84 +667,6 @@ describe("handleCompactCommand", () => {
expect(requireIncrementCompactionCountCall().compactionKind).toBe("server-endpoint");
});
it("accepts the successor session produced by context-engine accounting", async () => {
let currentSessionId = "native-session";
vi.mocked(isCurrentSessionEntry).mockImplementation(
({ expected }) => expected.sessionId === currentSessionId,
);
vi.mocked(incrementCompactionCount).mockImplementationOnce(async () => {
currentSessionId = "successor-session";
return 1;
});
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: true,
compactionKind: "context-engine",
result: {
summary: "compacted",
firstKeptEntryId: "first-kept",
sessionId: "successor-session",
tokensBefore: 999,
tokensAfter: 321,
},
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: {
sessionId: "native-session",
updatedAt: Date.now(),
},
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toMatchObject({ compacted: true, tokensAfter: 321 });
});
it("rejects a successor session when compaction accounting did not commit", async () => {
let currentSessionId = "native-session";
vi.mocked(isCurrentSessionEntry).mockImplementation(
({ expected }) => expected.sessionId === currentSessionId,
);
vi.mocked(incrementCompactionCount).mockImplementationOnce(async () => {
currentSessionId = "successor-session";
return undefined;
});
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: true,
compacted: true,
compactionKind: "context-engine",
result: {
summary: "compacted",
firstKeptEntryId: "first-kept",
sessionId: "successor-session",
tokensBefore: 999,
tokensAfter: 321,
},
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
sessionEntry: { sessionId: "native-session", updatedAt: Date.now() },
} as HandleCommandsParams,
true,
);
expect(result?.sessionCompaction).toEqual({
compacted: false,
reason: "command session changed",
});
});
it.each([
{
owner: "Codex",