diff --git a/src/agents/compaction.e2e.test.ts b/src/agents/compaction.e2e.test.ts index 877a48f8a1..de5f4ec4db 100644 --- a/src/agents/compaction.e2e.test.ts +++ b/src/agents/compaction.e2e.test.ts @@ -14,14 +14,25 @@ function makeMessage(id: number, size: number): AgentMessage { }; } +function makeMessages(count: number, size: number): AgentMessage[] { + return Array.from({ length: count }, (_, index) => makeMessage(index + 1, size)); +} + +function pruneLargeSimpleHistory() { + const messages = makeMessages(4, 4000); + const maxContextTokens = 2000; // budget is 1000 tokens (50%) + const pruned = pruneHistoryForContextShare({ + messages, + maxContextTokens, + maxHistoryShare: 0.5, + parts: 2, + }); + return { messages, pruned, maxContextTokens }; +} + describe("splitMessagesByTokenShare", () => { it("splits messages into two non-empty parts", () => { - const messages: AgentMessage[] = [ - makeMessage(1, 4000), - makeMessage(2, 4000), - makeMessage(3, 4000), - makeMessage(4, 4000), - ]; + const messages = makeMessages(4, 4000); const parts = splitMessagesByTokenShare(messages, 2); expect(parts.length).toBeGreaterThanOrEqual(2); @@ -31,14 +42,7 @@ describe("splitMessagesByTokenShare", () => { }); it("preserves message order across parts", () => { - const messages: AgentMessage[] = [ - makeMessage(1, 4000), - makeMessage(2, 4000), - makeMessage(3, 4000), - makeMessage(4, 4000), - makeMessage(5, 4000), - makeMessage(6, 4000), - ]; + const messages = makeMessages(6, 4000); const parts = splitMessagesByTokenShare(messages, 3); expect(parts.flat().map((msg) => msg.timestamp)).toEqual(messages.map((msg) => msg.timestamp)); @@ -47,19 +51,7 @@ describe("splitMessagesByTokenShare", () => { describe("pruneHistoryForContextShare", () => { it("drops older chunks until the history budget is met", () => { - const messages: AgentMessage[] = [ - makeMessage(1, 4000), - makeMessage(2, 4000), - makeMessage(3, 4000), - makeMessage(4, 4000), - ]; - const maxContextTokens = 2000; // budget is 1000 tokens (50%) - const pruned = pruneHistoryForContextShare({ - messages, - maxContextTokens, - maxHistoryShare: 0.5, - parts: 2, - }); + const { pruned, maxContextTokens } = pruneLargeSimpleHistory(); expect(pruned.droppedChunks).toBeGreaterThan(0); expect(pruned.keptTokens).toBeLessThanOrEqual(Math.floor(maxContextTokens * 0.5)); @@ -67,14 +59,7 @@ describe("pruneHistoryForContextShare", () => { }); it("keeps the newest messages when pruning", () => { - const messages: AgentMessage[] = [ - makeMessage(1, 4000), - makeMessage(2, 4000), - makeMessage(3, 4000), - makeMessage(4, 4000), - makeMessage(5, 4000), - makeMessage(6, 4000), - ]; + const messages = makeMessages(6, 4000); const totalTokens = estimateMessagesTokens(messages); const maxContextTokens = Math.max(1, Math.floor(totalTokens * 0.5)); // budget = 25% const pruned = pruneHistoryForContextShare({ @@ -110,19 +95,7 @@ describe("pruneHistoryForContextShare", () => { // When orphaned tool_results exist, droppedMessages may exceed // droppedMessagesList.length since orphans are counted but not // added to the list (they lack context for summarization). - const messages: AgentMessage[] = [ - makeMessage(1, 4000), - makeMessage(2, 4000), - makeMessage(3, 4000), - makeMessage(4, 4000), - ]; - const maxContextTokens = 2000; // budget is 1000 tokens (50%) - const pruned = pruneHistoryForContextShare({ - messages, - maxContextTokens, - maxHistoryShare: 0.5, - parts: 2, - }); + const { messages, pruned } = pruneLargeSimpleHistory(); expect(pruned.droppedChunks).toBeGreaterThan(0); // Without orphaned tool_results, counts match exactly diff --git a/src/agents/identity-avatar.e2e.test.ts b/src/agents/identity-avatar.e2e.test.ts index bb9404395f..2e06c545ff 100644 --- a/src/agents/identity-avatar.e2e.test.ts +++ b/src/agents/identity-avatar.e2e.test.ts @@ -10,6 +10,20 @@ async function writeFile(filePath: string, contents = "avatar") { await fs.writeFile(filePath, contents, "utf-8"); } +async function expectLocalAvatarPath( + cfg: OpenClawConfig, + workspace: string, + expectedRelativePath: string, +) { + const workspaceReal = await fs.realpath(workspace); + const resolved = resolveAgentAvatar(cfg, "main"); + expect(resolved.kind).toBe("local"); + if (resolved.kind === "local") { + const resolvedReal = await fs.realpath(resolved.filePath); + expect(path.relative(workspaceReal, resolvedReal)).toBe(expectedRelativePath); + } +} + describe("resolveAgentAvatar", () => { it("resolves local avatar from config when inside workspace", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-")); @@ -29,13 +43,7 @@ describe("resolveAgentAvatar", () => { }, }; - const workspaceReal = await fs.realpath(workspace); - const resolved = resolveAgentAvatar(cfg, "main"); - expect(resolved.kind).toBe("local"); - if (resolved.kind === "local") { - const resolvedReal = await fs.realpath(resolved.filePath); - expect(path.relative(workspaceReal, resolvedReal)).toBe(path.join("avatars", "main.png")); - } + await expectLocalAvatarPath(cfg, workspace, path.join("avatars", "main.png")); }); it("rejects avatars outside the workspace", async () => { @@ -82,12 +90,24 @@ describe("resolveAgentAvatar", () => { }, }; - const workspaceReal = await fs.realpath(workspace); + await expectLocalAvatarPath(cfg, workspace, path.join("avatars", "fallback.png")); + }); + + it("returns missing for non-existent local avatar files", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-")); + const workspace = path.join(root, "work"); + await fs.mkdir(workspace, { recursive: true }); + + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "main", workspace, identity: { avatar: "avatars/missing.png" } }], + }, + }; + const resolved = resolveAgentAvatar(cfg, "main"); - expect(resolved.kind).toBe("local"); - if (resolved.kind === "local") { - const resolvedReal = await fs.realpath(resolved.filePath); - expect(path.relative(workspaceReal, resolvedReal)).toBe(path.join("avatars", "fallback.png")); + expect(resolved.kind).toBe("none"); + if (resolved.kind === "none") { + expect(resolved.reason).toBe("missing"); } });