feat(wave2): @mosaic/openclaw-context plugin migrated to monorepo (#3)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #3.
This commit is contained in:
414
plugins/openclaw-context/tests/engine.test.ts
Normal file
414
plugins/openclaw-context/tests/engine.test.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OpenBrainConfigError } from "../src/errors.js";
|
||||
import { OpenBrainContextEngine } from "../src/engine.js";
|
||||
import type { AgentMessage } from "../src/openclaw-types.js";
|
||||
import type {
|
||||
OpenBrainClientLike,
|
||||
OpenBrainThought,
|
||||
OpenBrainThoughtInput,
|
||||
} from "../src/openbrain-client.js";
|
||||
|
||||
function makeThought(
|
||||
id: string,
|
||||
content: string,
|
||||
sessionId: string,
|
||||
role: string,
|
||||
createdAt: string,
|
||||
): OpenBrainThought {
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
source: `openclaw:${sessionId}`,
|
||||
metadata: {
|
||||
sessionId,
|
||||
role,
|
||||
type: "message",
|
||||
},
|
||||
createdAt,
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMockClient(): OpenBrainClientLike {
|
||||
return {
|
||||
createThought: vi.fn(async (input: OpenBrainThoughtInput) => ({
|
||||
id: `thought-${Math.random().toString(36).slice(2)}`,
|
||||
content: input.content,
|
||||
source: input.source,
|
||||
metadata: input.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
})),
|
||||
search: vi.fn(async () => []),
|
||||
listRecent: vi.fn(async () => []),
|
||||
updateThought: vi.fn(async (id, payload) => ({
|
||||
id,
|
||||
content: payload.content ?? "",
|
||||
source: "openclaw:session",
|
||||
metadata: payload.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
})),
|
||||
deleteThought: vi.fn(async () => undefined),
|
||||
deleteThoughts: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionId = "session-main";
|
||||
|
||||
const userMessage: AgentMessage = {
|
||||
role: "user",
|
||||
content: "What did we decide yesterday?",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
describe("OpenBrainContextEngine", () => {
|
||||
it("throws OpenBrainConfigError at bootstrap when baseUrl/apiKey are missing", async () => {
|
||||
const engine = new OpenBrainContextEngine({});
|
||||
|
||||
await expect(
|
||||
engine.bootstrap({
|
||||
sessionId,
|
||||
sessionFile: "/tmp/session.json",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(OpenBrainConfigError);
|
||||
});
|
||||
|
||||
it("ingests messages with session metadata", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
source: "openclaw",
|
||||
},
|
||||
{
|
||||
createClient: () => mockClient,
|
||||
},
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingest({ sessionId, message: userMessage });
|
||||
|
||||
expect(result.ingested).toBe(true);
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
sessionId,
|
||||
role: "user",
|
||||
type: "message",
|
||||
turn: 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ingests batches and returns ingested count", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingestBatch({
|
||||
sessionId,
|
||||
messages: [
|
||||
{ role: "user", content: "one", timestamp: 1 },
|
||||
{ role: "assistant", content: "two", timestamp: 2 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.ingestedCount).toBe(2);
|
||||
expect(mockClient.createThought).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("ingests batches in parallel chunks of five", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
let inFlight = 0;
|
||||
let maxInFlight = 0;
|
||||
let createdCount = 0;
|
||||
|
||||
vi.mocked(mockClient.createThought).mockImplementation(async (input: OpenBrainThoughtInput) => {
|
||||
inFlight += 1;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 20);
|
||||
});
|
||||
inFlight -= 1;
|
||||
createdCount += 1;
|
||||
return {
|
||||
id: `thought-${createdCount}`,
|
||||
content: input.content,
|
||||
source: input.source,
|
||||
metadata: input.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingestBatch({
|
||||
sessionId,
|
||||
messages: Array.from({ length: 10 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? "user" : "assistant",
|
||||
content: `message-${index + 1}`,
|
||||
timestamp: index + 1,
|
||||
})),
|
||||
});
|
||||
|
||||
expect(result.ingestedCount).toBe(10);
|
||||
expect(maxInFlight).toBe(5);
|
||||
expect(mockClient.createThought).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it("assembles context from recent + semantic search, deduped and budget-aware", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "recent user context", sessionId, "user", "2026-03-06T12:00:00.000Z"),
|
||||
makeThought(
|
||||
"t2",
|
||||
"recent assistant context",
|
||||
sessionId,
|
||||
"assistant",
|
||||
"2026-03-06T12:01:00.000Z",
|
||||
),
|
||||
]);
|
||||
vi.mocked(mockClient.search).mockResolvedValue([
|
||||
makeThought(
|
||||
"t2",
|
||||
"recent assistant context",
|
||||
sessionId,
|
||||
"assistant",
|
||||
"2026-03-06T12:01:00.000Z",
|
||||
),
|
||||
makeThought("t3", "semantic match", sessionId, "assistant", "2026-03-06T12:02:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
recentMessages: 10,
|
||||
semanticSearchLimit: 10,
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const result = await engine.assemble({
|
||||
sessionId,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Find the semantic context",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tokenBudget: 40,
|
||||
});
|
||||
|
||||
expect(mockClient.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: "Find the semantic context",
|
||||
limit: 10,
|
||||
}),
|
||||
);
|
||||
expect(result.estimatedTokens).toBeLessThanOrEqual(40);
|
||||
expect(result.messages.map((message) => String(message.content))).toEqual([
|
||||
"recent user context",
|
||||
"recent assistant context",
|
||||
"semantic match",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compact archives a summary thought and deletes summarized inputs", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue(
|
||||
Array.from({ length: 12 }, (_, index) => {
|
||||
return makeThought(
|
||||
`t${index + 1}`,
|
||||
`message ${index + 1}`,
|
||||
sessionId,
|
||||
index % 2 === 0 ? "user" : "assistant",
|
||||
`2026-03-06T12:${String(index).padStart(2, "0")}:00.000Z`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const result = await engine.compact({
|
||||
sessionId,
|
||||
sessionFile: "/tmp/session.json",
|
||||
tokenBudget: 128,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
sessionId,
|
||||
type: "summary",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const deletedIds = vi
|
||||
.mocked(mockClient.deleteThought)
|
||||
.mock.calls.map(([id]) => id)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
expect(deletedIds).toEqual([
|
||||
"t10",
|
||||
"t11",
|
||||
"t12",
|
||||
"t3",
|
||||
"t4",
|
||||
"t5",
|
||||
"t6",
|
||||
"t7",
|
||||
"t8",
|
||||
"t9",
|
||||
]);
|
||||
});
|
||||
|
||||
it("stops trimming once the newest message exceeds budget", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const oversizedNewest = "z".repeat(400);
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "small older message", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
makeThought("t2", oversizedNewest, sessionId, "assistant", "2026-03-06T12:01:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.assemble({
|
||||
sessionId,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "query",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tokenBudget: 12,
|
||||
});
|
||||
|
||||
expect(result.messages.map((message) => String(message.content))).toEqual([oversizedNewest]);
|
||||
});
|
||||
|
||||
it("prepares subagent spawn and rollback deletes seeded context", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "parent context", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
]);
|
||||
vi.mocked(mockClient.createThought).mockResolvedValue({
|
||||
id: "seed-thought",
|
||||
content: "seed",
|
||||
source: "openclaw:child",
|
||||
metadata: undefined,
|
||||
createdAt: "2026-03-06T12:01:00.000Z",
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
});
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const prep = await engine.prepareSubagentSpawn({
|
||||
parentSessionKey: sessionId,
|
||||
childSessionKey: "child-session",
|
||||
});
|
||||
|
||||
expect(prep).toBeDefined();
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:child-session",
|
||||
}),
|
||||
);
|
||||
|
||||
await prep?.rollback();
|
||||
expect(mockClient.deleteThought).toHaveBeenCalledWith("seed-thought");
|
||||
});
|
||||
|
||||
it("stores child outcome back into parent on subagent end", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent)
|
||||
.mockResolvedValueOnce([
|
||||
makeThought("p1", "parent context", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
makeThought("c1", "child result detail", "child-session", "assistant", "2026-03-06T12:05:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
await engine.prepareSubagentSpawn({
|
||||
parentSessionKey: sessionId,
|
||||
childSessionKey: "child-session",
|
||||
});
|
||||
|
||||
await engine.onSubagentEnded({
|
||||
childSessionKey: "child-session",
|
||||
reason: "completed",
|
||||
});
|
||||
|
||||
expect(mockClient.createThought).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
type: "subagent-result",
|
||||
sessionId,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
81
plugins/openclaw-context/tests/openbrain-client.test.ts
Normal file
81
plugins/openclaw-context/tests/openbrain-client.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OpenBrainConfigError, OpenBrainHttpError } from "../src/errors.js";
|
||||
import { OpenBrainClient } from "../src/openbrain-client.js";
|
||||
|
||||
function jsonResponse(body: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: init?.status ?? 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("OpenBrainClient", () => {
|
||||
it("sends bearer auth and normalized URL for createThought", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
jsonResponse({
|
||||
id: "thought-1",
|
||||
content: "hello",
|
||||
source: "openclaw:main",
|
||||
}),
|
||||
);
|
||||
|
||||
const client = new OpenBrainClient({
|
||||
baseUrl: "https://brain.example.com/",
|
||||
apiKey: "secret",
|
||||
fetchImpl: fetchMock as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
await client.createThought({
|
||||
content: "hello",
|
||||
source: "openclaw:main",
|
||||
metadata: { sessionId: "session-1" },
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const firstCall = fetchMock.mock.calls[0];
|
||||
expect(firstCall).toBeDefined();
|
||||
if (firstCall === undefined) {
|
||||
throw new Error("Expected fetch call arguments");
|
||||
}
|
||||
const [url, init] = firstCall as unknown as [string, RequestInit];
|
||||
expect(url).toBe("https://brain.example.com/v1/thoughts");
|
||||
expect(init.method).toBe("POST");
|
||||
expect(init.headers).toMatchObject({
|
||||
Authorization: "Bearer secret",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws OpenBrainHttpError on non-2xx responses", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
jsonResponse({ error: "unauthorized" }, { status: 401 }),
|
||||
);
|
||||
|
||||
const client = new OpenBrainClient({
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
fetchImpl: fetchMock as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
await expect(client.listRecent({ limit: 5, source: "openclaw:main" })).rejects.toBeInstanceOf(
|
||||
OpenBrainHttpError,
|
||||
);
|
||||
|
||||
await expect(client.listRecent({ limit: 5, source: "openclaw:main" })).rejects.toMatchObject({
|
||||
status: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws OpenBrainConfigError when initialized without baseUrl or apiKey", () => {
|
||||
expect(
|
||||
() => new OpenBrainClient({ baseUrl: "", apiKey: "secret", fetchImpl: fetch }),
|
||||
).toThrow(OpenBrainConfigError);
|
||||
expect(
|
||||
() => new OpenBrainClient({ baseUrl: "https://brain.example.com", apiKey: "", fetchImpl: fetch }),
|
||||
).toThrow(OpenBrainConfigError);
|
||||
});
|
||||
});
|
||||
30
plugins/openclaw-context/tests/register.test.ts
Normal file
30
plugins/openclaw-context/tests/register.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OPENBRAIN_CONTEXT_ENGINE_ID, register } from "../src/index.js";
|
||||
|
||||
describe("plugin register()", () => {
|
||||
it("registers the openbrain context engine factory", async () => {
|
||||
const registerContextEngine = vi.fn();
|
||||
|
||||
register({
|
||||
registerContextEngine,
|
||||
pluginConfig: {
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(registerContextEngine).toHaveBeenCalledTimes(1);
|
||||
const [id, factory] = registerContextEngine.mock.calls[0] as [string, () => Promise<unknown> | unknown];
|
||||
expect(id).toBe(OPENBRAIN_CONTEXT_ENGINE_ID);
|
||||
|
||||
const engine = await factory();
|
||||
expect(engine).toHaveProperty("info.id", OPENBRAIN_CONTEXT_ENGINE_ID);
|
||||
});
|
||||
});
|
||||
9
plugins/openclaw-context/tests/smoke.test.ts
Normal file
9
plugins/openclaw-context/tests/smoke.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { OPENBRAIN_CONTEXT_ENGINE_ID } from "../src/index.js";
|
||||
|
||||
describe("project scaffold", () => {
|
||||
it("exports openbrain context engine id", () => {
|
||||
expect(OPENBRAIN_CONTEXT_ENGINE_ID).toBe("openbrain");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user