Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,015 changes: 1,015 additions & 0 deletions examples/openclaw-plugin/tests/e2e/test-archive-expand.py

Large diffs are not rendered by default.

221 changes: 221 additions & 0 deletions examples/openclaw-plugin/tests/ut/build-memory-lines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { describe, expect, it, vi } from "vitest";

import {
estimateTokenCount,
buildMemoryLines,
buildMemoryLinesWithBudget,
} from "../../index.js";
import type { FindResultItem } from "../../client.js";

function makeMemory(overrides?: Partial<FindResultItem>): FindResultItem {
return {
uri: "viking://user/memories/test-1",
level: 2,
abstract: "Test memory abstract",
category: "core",
score: 0.85,
...overrides,
};
}

describe("estimateTokenCount", () => {
it("returns 0 for empty string", () => {
expect(estimateTokenCount("")).toBe(0);
});

it("estimates tokens as ceil(chars/4)", () => {
expect(estimateTokenCount("hello")).toBe(2); // ceil(5/4)
expect(estimateTokenCount("abcd")).toBe(1); // ceil(4/4)
expect(estimateTokenCount("abcde")).toBe(2); // ceil(5/4)
});

it("handles long text", () => {
const text = "a".repeat(1000);
expect(estimateTokenCount(text)).toBe(250);
});
});

describe("buildMemoryLines", () => {
it("formats memories with category and content", async () => {
const memories = [
makeMemory({ category: "preferences", abstract: "User prefers Python" }),
makeMemory({ category: "facts", abstract: "Works at TechCorp" }),
];
const readFn = vi.fn();

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: true,
recallMaxContentChars: 500,
});

expect(lines).toHaveLength(2);
expect(lines[0]).toBe("- [preferences] User prefers Python");
expect(lines[1]).toBe("- [facts] Works at TechCorp");
});

it("uses abstract when recallPreferAbstract=true", async () => {
const memories = [makeMemory({ abstract: "The abstract text" })];
const readFn = vi.fn();

await buildMemoryLines(memories, readFn, {
recallPreferAbstract: true,
recallMaxContentChars: 500,
});

expect(readFn).not.toHaveBeenCalled();
});

it("calls readFn for level=2 when recallPreferAbstract=false", async () => {
const memories = [makeMemory({ level: 2, abstract: "fallback" })];
const readFn = vi.fn().mockResolvedValue("Full content from readFn");

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: false,
recallMaxContentChars: 500,
});

expect(readFn).toHaveBeenCalledWith("viking://user/memories/test-1");
expect(lines[0]).toContain("Full content from readFn");
});

it("falls back to abstract when readFn throws", async () => {
const memories = [makeMemory({ level: 2, abstract: "Fallback abstract" })];
const readFn = vi.fn().mockRejectedValue(new Error("network error"));

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: false,
recallMaxContentChars: 500,
});

expect(lines[0]).toContain("Fallback abstract");
});

it("falls back to abstract when readFn returns empty", async () => {
const memories = [makeMemory({ level: 2, abstract: "Fallback abstract" })];
const readFn = vi.fn().mockResolvedValue("");

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: false,
recallMaxContentChars: 500,
});

expect(lines[0]).toContain("Fallback abstract");
});

it("truncates content exceeding recallMaxContentChars", async () => {
const longAbstract = "x".repeat(600);
const memories = [makeMemory({ abstract: longAbstract })];
const readFn = vi.fn();

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: true,
recallMaxContentChars: 100,
});

expect(lines[0]).toContain("...");
expect(lines[0].length).toBeLessThan(600);
});

it("uses uri as fallback when no abstract", async () => {
const memories = [makeMemory({ abstract: "", level: 1 })];
const readFn = vi.fn();

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: true,
recallMaxContentChars: 500,
});

expect(lines[0]).toContain("viking://user/memories/test-1");
});

it("defaults category to 'memory'", async () => {
const memories = [makeMemory({ category: undefined })];
const readFn = vi.fn();

const lines = await buildMemoryLines(memories, readFn, {
recallPreferAbstract: true,
recallMaxContentChars: 500,
});

expect(lines[0]).toContain("[memory]");
});
});

describe("buildMemoryLinesWithBudget", () => {
it("stops adding when budget is exhausted", async () => {
const memories = [
makeMemory({ abstract: "a".repeat(100), category: "a" }),
makeMemory({ abstract: "b".repeat(100), category: "b" }),
makeMemory({ abstract: "c".repeat(100), category: "c" }),
];
const readFn = vi.fn();
// Each line ~100 chars → ~25 tokens. Budget=40 fits 1-2 lines.
const { lines, estimatedTokens } = await buildMemoryLinesWithBudget(
memories,
readFn,
{
recallPreferAbstract: true,
recallMaxContentChars: 500,
recallTokenBudget: 40,
},
);

expect(lines.length).toBeLessThan(3);
expect(estimatedTokens).toBeLessThanOrEqual(40 + 30); // first always included even if over
});

it("always includes the first memory even if over budget", async () => {
const memories = [
makeMemory({ abstract: "a".repeat(400) }), // ~100 tokens
];
const readFn = vi.fn();

const { lines } = await buildMemoryLinesWithBudget(
memories,
readFn,
{
recallPreferAbstract: true,
recallMaxContentChars: 500,
recallTokenBudget: 10,
},
);

expect(lines).toHaveLength(1);
});

it("returns correct estimatedTokens sum", async () => {
const memories = [
makeMemory({ abstract: "short" }),
];
const readFn = vi.fn();

const { lines, estimatedTokens } = await buildMemoryLinesWithBudget(
memories,
readFn,
{
recallPreferAbstract: true,
recallMaxContentChars: 500,
recallTokenBudget: 2000,
},
);

expect(lines).toHaveLength(1);
expect(estimatedTokens).toBe(estimateTokenCount(lines[0]!));
});

it("handles empty memories array", async () => {
const readFn = vi.fn();
const { lines, estimatedTokens } = await buildMemoryLinesWithBudget(
[],
readFn,
{
recallPreferAbstract: true,
recallMaxContentChars: 500,
recallTokenBudget: 2000,
},
);

expect(lines).toHaveLength(0);
expect(estimatedTokens).toBe(0);
});
});
49 changes: 49 additions & 0 deletions examples/openclaw-plugin/tests/ut/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";

import { isMemoryUri } from "../../client.js";

describe("isMemoryUri", () => {
it("returns true for valid user memory URI", () => {
expect(isMemoryUri("viking://user/memories/abc-123")).toBe(true);
});

it("returns true for user memory URI with space prefix", () => {
expect(isMemoryUri("viking://user/default/memories/item-1")).toBe(true);
});

it("returns true for valid agent memory URI", () => {
expect(isMemoryUri("viking://agent/memories/xyz")).toBe(true);
});

it("returns true for agent memory URI with space prefix", () => {
expect(isMemoryUri("viking://agent/abc123/memories/item-2")).toBe(true);
});

it("returns true for user memories root", () => {
expect(isMemoryUri("viking://user/memories")).toBe(true);
});

it("returns true for user memories trailing slash", () => {
expect(isMemoryUri("viking://user/memories/")).toBe(true);
});

it("returns false for user skills URI", () => {
expect(isMemoryUri("viking://user/skills/abc")).toBe(false);
});

it("returns false for agent instructions URI", () => {
expect(isMemoryUri("viking://agent/instructions/rule-1")).toBe(false);
});

it("returns false for empty string", () => {
expect(isMemoryUri("")).toBe(false);
});

it("returns false for random URL", () => {
expect(isMemoryUri("http://example.com/memories")).toBe(false);
});

it("returns false for partial viking URI without scope", () => {
expect(isMemoryUri("viking://memories/abc")).toBe(false);
});
});
Loading
Loading