mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 06:42:44 +00:00
test(agents): dedupe avatar and compaction fixtures
This commit is contained in:
@@ -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", () => {
|
describe("splitMessagesByTokenShare", () => {
|
||||||
it("splits messages into two non-empty parts", () => {
|
it("splits messages into two non-empty parts", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages = makeMessages(4, 4000);
|
||||||
makeMessage(1, 4000),
|
|
||||||
makeMessage(2, 4000),
|
|
||||||
makeMessage(3, 4000),
|
|
||||||
makeMessage(4, 4000),
|
|
||||||
];
|
|
||||||
|
|
||||||
const parts = splitMessagesByTokenShare(messages, 2);
|
const parts = splitMessagesByTokenShare(messages, 2);
|
||||||
expect(parts.length).toBeGreaterThanOrEqual(2);
|
expect(parts.length).toBeGreaterThanOrEqual(2);
|
||||||
@@ -31,14 +42,7 @@ describe("splitMessagesByTokenShare", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves message order across parts", () => {
|
it("preserves message order across parts", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages = makeMessages(6, 4000);
|
||||||
makeMessage(1, 4000),
|
|
||||||
makeMessage(2, 4000),
|
|
||||||
makeMessage(3, 4000),
|
|
||||||
makeMessage(4, 4000),
|
|
||||||
makeMessage(5, 4000),
|
|
||||||
makeMessage(6, 4000),
|
|
||||||
];
|
|
||||||
|
|
||||||
const parts = splitMessagesByTokenShare(messages, 3);
|
const parts = splitMessagesByTokenShare(messages, 3);
|
||||||
expect(parts.flat().map((msg) => msg.timestamp)).toEqual(messages.map((msg) => msg.timestamp));
|
expect(parts.flat().map((msg) => msg.timestamp)).toEqual(messages.map((msg) => msg.timestamp));
|
||||||
@@ -47,19 +51,7 @@ describe("splitMessagesByTokenShare", () => {
|
|||||||
|
|
||||||
describe("pruneHistoryForContextShare", () => {
|
describe("pruneHistoryForContextShare", () => {
|
||||||
it("drops older chunks until the history budget is met", () => {
|
it("drops older chunks until the history budget is met", () => {
|
||||||
const messages: AgentMessage[] = [
|
const { pruned, maxContextTokens } = pruneLargeSimpleHistory();
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pruned.droppedChunks).toBeGreaterThan(0);
|
expect(pruned.droppedChunks).toBeGreaterThan(0);
|
||||||
expect(pruned.keptTokens).toBeLessThanOrEqual(Math.floor(maxContextTokens * 0.5));
|
expect(pruned.keptTokens).toBeLessThanOrEqual(Math.floor(maxContextTokens * 0.5));
|
||||||
@@ -67,14 +59,7 @@ describe("pruneHistoryForContextShare", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the newest messages when pruning", () => {
|
it("keeps the newest messages when pruning", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages = makeMessages(6, 4000);
|
||||||
makeMessage(1, 4000),
|
|
||||||
makeMessage(2, 4000),
|
|
||||||
makeMessage(3, 4000),
|
|
||||||
makeMessage(4, 4000),
|
|
||||||
makeMessage(5, 4000),
|
|
||||||
makeMessage(6, 4000),
|
|
||||||
];
|
|
||||||
const totalTokens = estimateMessagesTokens(messages);
|
const totalTokens = estimateMessagesTokens(messages);
|
||||||
const maxContextTokens = Math.max(1, Math.floor(totalTokens * 0.5)); // budget = 25%
|
const maxContextTokens = Math.max(1, Math.floor(totalTokens * 0.5)); // budget = 25%
|
||||||
const pruned = pruneHistoryForContextShare({
|
const pruned = pruneHistoryForContextShare({
|
||||||
@@ -110,19 +95,7 @@ describe("pruneHistoryForContextShare", () => {
|
|||||||
// When orphaned tool_results exist, droppedMessages may exceed
|
// When orphaned tool_results exist, droppedMessages may exceed
|
||||||
// droppedMessagesList.length since orphans are counted but not
|
// droppedMessagesList.length since orphans are counted but not
|
||||||
// added to the list (they lack context for summarization).
|
// added to the list (they lack context for summarization).
|
||||||
const messages: AgentMessage[] = [
|
const { messages, pruned } = pruneLargeSimpleHistory();
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pruned.droppedChunks).toBeGreaterThan(0);
|
expect(pruned.droppedChunks).toBeGreaterThan(0);
|
||||||
// Without orphaned tool_results, counts match exactly
|
// Without orphaned tool_results, counts match exactly
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ async function writeFile(filePath: string, contents = "avatar") {
|
|||||||
await fs.writeFile(filePath, contents, "utf-8");
|
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", () => {
|
describe("resolveAgentAvatar", () => {
|
||||||
it("resolves local avatar from config when inside workspace", async () => {
|
it("resolves local avatar from config when inside workspace", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-"));
|
||||||
@@ -29,13 +43,7 @@ describe("resolveAgentAvatar", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const workspaceReal = await fs.realpath(workspace);
|
await expectLocalAvatarPath(cfg, workspace, path.join("avatars", "main.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", "main.png"));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects avatars outside the workspace", async () => {
|
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");
|
const resolved = resolveAgentAvatar(cfg, "main");
|
||||||
expect(resolved.kind).toBe("local");
|
expect(resolved.kind).toBe("none");
|
||||||
if (resolved.kind === "local") {
|
if (resolved.kind === "none") {
|
||||||
const resolvedReal = await fs.realpath(resolved.filePath);
|
expect(resolved.reason).toBe("missing");
|
||||||
expect(path.relative(workspaceReal, resolvedReal)).toBe(path.join("avatars", "fallback.png"));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user