fix: openclaw plugin and fix the login section there (#4696)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Tests for cli/config-file.ts — file-based config helpers.
|
||||
*
|
||||
* All filesystem operations are mocked via fs-safe.ts so tests never
|
||||
* touch the real disk.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../fs-safe.ts", () => ({
|
||||
readText: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
writeText: vi.fn(),
|
||||
mkdirp: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
import { readText, exists, writeText, mkdirp } from "../fs-safe.ts";
|
||||
import {
|
||||
readPluginAuth,
|
||||
writePluginAuth,
|
||||
getBaseUrl,
|
||||
DEFAULT_BASE_URL,
|
||||
} from "../cli/config-file.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockExists = exists as ReturnType<typeof vi.fn>;
|
||||
const mockReadText = readText as ReturnType<typeof vi.fn>;
|
||||
const mockWriteText = writeText as ReturnType<typeof vi.fn>;
|
||||
const mockMkdirp = mkdirp as ReturnType<typeof vi.fn>;
|
||||
|
||||
function setConfigFile(obj: Record<string, unknown>): void {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockReadText.mockReturnValue(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function setNoFile(): void {
|
||||
mockExists.mockReturnValue(false);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// readPluginAuth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("readPluginAuth", () => {
|
||||
it("returns empty object when config file does not exist", () => {
|
||||
setNoFile();
|
||||
expect(readPluginAuth()).toEqual({});
|
||||
});
|
||||
|
||||
it("returns empty object when config has no plugins section", () => {
|
||||
setConfigFile({ someOtherKey: true });
|
||||
expect(readPluginAuth()).toEqual({});
|
||||
});
|
||||
|
||||
it("reads all fields correctly from nested config", () => {
|
||||
setConfigFile({
|
||||
plugins: {
|
||||
entries: {
|
||||
"openclaw-mem0": {
|
||||
enabled: true,
|
||||
config: {
|
||||
apiKey: "sk-test-123",
|
||||
baseUrl: "https://custom.api.com",
|
||||
userId: "user-1",
|
||||
orgId: "org-1",
|
||||
projectId: "proj-1",
|
||||
mode: "platform",
|
||||
enableGraph: true,
|
||||
autoRecall: true,
|
||||
autoCapture: false,
|
||||
topK: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const auth = readPluginAuth();
|
||||
expect(auth).toEqual({
|
||||
apiKey: "sk-test-123",
|
||||
baseUrl: "https://custom.api.com",
|
||||
userId: "user-1",
|
||||
orgId: "org-1",
|
||||
projectId: "proj-1",
|
||||
mode: "platform",
|
||||
enableGraph: true,
|
||||
autoRecall: true,
|
||||
autoCapture: false,
|
||||
topK: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles snake_case aliases (api_key, base_url, user_id, org_id, project_id)", () => {
|
||||
setConfigFile({
|
||||
plugins: {
|
||||
entries: {
|
||||
"openclaw-mem0": {
|
||||
enabled: true,
|
||||
config: {
|
||||
api_key: "sk-snake",
|
||||
base_url: "https://snake.api.com",
|
||||
user_id: "user-snake",
|
||||
org_id: "org-snake",
|
||||
project_id: "proj-snake",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const auth = readPluginAuth();
|
||||
expect(auth.apiKey).toBe("sk-snake");
|
||||
expect(auth.baseUrl).toBe("https://snake.api.com");
|
||||
expect(auth.userId).toBe("user-snake");
|
||||
expect(auth.orgId).toBe("org-snake");
|
||||
expect(auth.projectId).toBe("proj-snake");
|
||||
});
|
||||
|
||||
it("returns empty object when JSON is invalid", () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockReadText.mockReturnValue("not valid json {{{");
|
||||
expect(readPluginAuth()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// writePluginAuth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("writePluginAuth", () => {
|
||||
it("creates nested structure from scratch when no config exists", () => {
|
||||
setNoFile();
|
||||
// exists returns false for both the file (readFullConfig) and the dir (writeFullConfig)
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
writePluginAuth({ apiKey: "sk-new", userId: "u1" });
|
||||
|
||||
expect(mockMkdirp).toHaveBeenCalled();
|
||||
expect(mockWriteText).toHaveBeenCalledOnce();
|
||||
|
||||
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
|
||||
expect(written.plugins.entries["openclaw-mem0"].enabled).toBe(true);
|
||||
expect(written.plugins.entries["openclaw-mem0"].config.apiKey).toBe(
|
||||
"sk-new",
|
||||
);
|
||||
expect(written.plugins.entries["openclaw-mem0"].config.userId).toBe("u1");
|
||||
});
|
||||
|
||||
it("merges into existing config preserving other data", () => {
|
||||
setConfigFile({
|
||||
otherSetting: "keep-me",
|
||||
plugins: {
|
||||
entries: {
|
||||
"openclaw-mem0": {
|
||||
enabled: true,
|
||||
config: {
|
||||
apiKey: "sk-old",
|
||||
mode: "platform",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
writePluginAuth({ baseUrl: "https://new.api.com" });
|
||||
|
||||
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
|
||||
// Existing fields preserved
|
||||
expect(written.otherSetting).toBe("keep-me");
|
||||
expect(written.plugins.entries["openclaw-mem0"].config.apiKey).toBe(
|
||||
"sk-old",
|
||||
);
|
||||
expect(written.plugins.entries["openclaw-mem0"].config.mode).toBe(
|
||||
"platform",
|
||||
);
|
||||
// New field added
|
||||
expect(written.plugins.entries["openclaw-mem0"].config.baseUrl).toBe(
|
||||
"https://new.api.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates directory if missing", () => {
|
||||
// File doesn't exist (readFullConfig returns {}), dir doesn't exist
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
writePluginAuth({ apiKey: "sk-test" });
|
||||
|
||||
expect(mockMkdirp).toHaveBeenCalledWith(
|
||||
expect.stringContaining(".openclaw"),
|
||||
0o700,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips undefined values", () => {
|
||||
setNoFile();
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
writePluginAuth({
|
||||
apiKey: "sk-set",
|
||||
baseUrl: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
|
||||
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
|
||||
const cfg = written.plugins.entries["openclaw-mem0"].config;
|
||||
expect(cfg.apiKey).toBe("sk-set");
|
||||
expect(cfg).not.toHaveProperty("baseUrl");
|
||||
expect(cfg).not.toHaveProperty("userId");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getBaseUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getBaseUrl", () => {
|
||||
it("returns configured URL when baseUrl is set", () => {
|
||||
setConfigFile({
|
||||
plugins: {
|
||||
entries: {
|
||||
"openclaw-mem0": {
|
||||
enabled: true,
|
||||
config: { baseUrl: "https://custom.example.com" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getBaseUrl()).toBe("https://custom.example.com");
|
||||
});
|
||||
|
||||
it("returns default URL when baseUrl is not configured", () => {
|
||||
setNoFile();
|
||||
expect(getBaseUrl()).toBe(DEFAULT_BASE_URL);
|
||||
});
|
||||
});
|
||||
@@ -99,9 +99,9 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
|
||||
// mem0ConfigSchema.parse() — mode parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("mem0ConfigSchema.parse() — mode parsing", () => {
|
||||
it('"oss" is normalized to "open-source"', () => {
|
||||
const cfg = mem0ConfigSchema.parse({ mode: "oss" });
|
||||
expect(cfg.mode).toBe("open-source");
|
||||
it('"oss" is not a valid mode and defaults to "platform"', () => {
|
||||
const cfg = mem0ConfigSchema.parse({ mode: "oss", apiKey: "k" });
|
||||
expect(cfg.mode).toBe("platform");
|
||||
});
|
||||
|
||||
it('"open-source" stays as "open-source"', () => {
|
||||
@@ -184,8 +184,8 @@ describe("mem0ConfigSchema.parse() — needsSetup", () => {
|
||||
expect(cfg.needsSetup).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when mode is open-source (no apiKey needed)", () => {
|
||||
const cfg = mem0ConfigSchema.parse({ mode: "oss" });
|
||||
it("is false when mode is 'open-source' via explicit string (no apiKey needed)", () => {
|
||||
const cfg = mem0ConfigSchema.parse({ mode: "open-source" });
|
||||
expect(cfg.needsSetup).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Tests for dream-gate.ts — activity tracking, gate logic, and lock mechanism
|
||||
* for automatic memory consolidation.
|
||||
*
|
||||
* All filesystem operations are mocked via fs-safe.ts.
|
||||
* Time-dependent tests use vi.useFakeTimers().
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("../fs-safe.ts", () => ({
|
||||
readText: vi.fn(),
|
||||
writeText: vi.fn(),
|
||||
mkdirp: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
import { readText, writeText, mkdirp, unlink } from "../fs-safe.ts";
|
||||
import {
|
||||
incrementSessionCount,
|
||||
checkCheapGates,
|
||||
checkMemoryGate,
|
||||
acquireDreamLock,
|
||||
releaseDreamLock,
|
||||
recordDreamCompletion,
|
||||
getDreamState,
|
||||
} from "../dream-gate.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockReadText = readText as ReturnType<typeof vi.fn>;
|
||||
const mockWriteText = writeText as ReturnType<typeof vi.fn>;
|
||||
const mockMkdirp = mkdirp as ReturnType<typeof vi.fn>;
|
||||
const mockUnlink = unlink as ReturnType<typeof vi.fn>;
|
||||
|
||||
const STATE_DIR = "/tmp/test-state";
|
||||
|
||||
interface DreamState {
|
||||
lastConsolidatedAt: number;
|
||||
sessionsSince: number;
|
||||
lastSessionId: string | null;
|
||||
}
|
||||
|
||||
function setDreamState(state: DreamState): void {
|
||||
mockReadText.mockImplementation((filePath: string) => {
|
||||
if (filePath.endsWith("dream-state.json")) {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
}
|
||||
|
||||
function setNoState(): void {
|
||||
mockReadText.mockImplementation(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
}
|
||||
|
||||
function getWrittenState(): DreamState {
|
||||
const call = mockWriteText.mock.calls.find((c: unknown[]) =>
|
||||
(c[0] as string).endsWith("dream-state.json"),
|
||||
);
|
||||
if (!call) throw new Error("No state file written");
|
||||
return JSON.parse(call[1] as string);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockMkdirp.mockReturnValue(undefined);
|
||||
mockUnlink.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// incrementSessionCount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("incrementSessionCount", () => {
|
||||
it("increments counter for a new session", () => {
|
||||
setDreamState({
|
||||
lastConsolidatedAt: 0,
|
||||
sessionsSince: 3,
|
||||
lastSessionId: "session-old",
|
||||
});
|
||||
|
||||
incrementSessionCount(STATE_DIR, "session-new");
|
||||
|
||||
const written = getWrittenState();
|
||||
expect(written.sessionsSince).toBe(4);
|
||||
expect(written.lastSessionId).toBe("session-new");
|
||||
});
|
||||
|
||||
it("deduplicates same session (no increment)", () => {
|
||||
setDreamState({
|
||||
lastConsolidatedAt: 0,
|
||||
sessionsSince: 3,
|
||||
lastSessionId: "session-same",
|
||||
});
|
||||
|
||||
incrementSessionCount(STATE_DIR, "session-same");
|
||||
|
||||
// writeText should NOT have been called for the state file
|
||||
const stateWrites = mockWriteText.mock.calls.filter((c: unknown[]) =>
|
||||
(c[0] as string).endsWith("dream-state.json"),
|
||||
);
|
||||
expect(stateWrites).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkCheapGates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("checkCheapGates", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails time gate when consolidation was too recent", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// Last consolidated 1 hour ago, but minHours is 24
|
||||
setDreamState({
|
||||
lastConsolidatedAt: now - 1 * 3_600_000,
|
||||
sessionsSince: 100,
|
||||
lastSessionId: null,
|
||||
});
|
||||
|
||||
const result = checkCheapGates(STATE_DIR, { minHours: 24, minSessions: 5 });
|
||||
expect(result.proceed).toBe(false);
|
||||
expect(result.reason).toContain("time");
|
||||
});
|
||||
|
||||
it("fails session gate when too few sessions", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// Last consolidated 48 hours ago (passes time gate), but only 2 sessions
|
||||
setDreamState({
|
||||
lastConsolidatedAt: now - 48 * 3_600_000,
|
||||
sessionsSince: 2,
|
||||
lastSessionId: null,
|
||||
});
|
||||
|
||||
const result = checkCheapGates(STATE_DIR, {
|
||||
minHours: 24,
|
||||
minSessions: 5,
|
||||
});
|
||||
expect(result.proceed).toBe(false);
|
||||
expect(result.reason).toContain("sessions");
|
||||
});
|
||||
|
||||
it("passes both gates when conditions are met", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// 48 hours ago, 10 sessions — both gates pass
|
||||
setDreamState({
|
||||
lastConsolidatedAt: now - 48 * 3_600_000,
|
||||
sessionsSince: 10,
|
||||
lastSessionId: null,
|
||||
});
|
||||
|
||||
const result = checkCheapGates(STATE_DIR, {
|
||||
minHours: 24,
|
||||
minSessions: 5,
|
||||
});
|
||||
expect(result.proceed).toBe(true);
|
||||
expect(result.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses defaults when config is empty", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// Never consolidated (0), 100 sessions — should pass with defaults (24h, 5 sessions)
|
||||
setDreamState({
|
||||
lastConsolidatedAt: 0,
|
||||
sessionsSince: 100,
|
||||
lastSessionId: null,
|
||||
});
|
||||
|
||||
const result = checkCheapGates(STATE_DIR, {});
|
||||
expect(result.proceed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkMemoryGate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("checkMemoryGate", () => {
|
||||
it("fails when too few memories", () => {
|
||||
const result = checkMemoryGate(5, { minMemories: 20 });
|
||||
expect(result.pass).toBe(false);
|
||||
expect(result.reason).toContain("memories");
|
||||
expect(result.reason).toContain("5");
|
||||
});
|
||||
|
||||
it("passes when enough memories", () => {
|
||||
const result = checkMemoryGate(25, { minMemories: 20 });
|
||||
expect(result.pass).toBe(true);
|
||||
expect(result.reason).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// acquireDreamLock
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("acquireDreamLock", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("succeeds when no lock exists", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// readText throws for lock file (not found), writeText succeeds for wx create
|
||||
mockReadText.mockImplementation(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
mockWriteText.mockReturnValue(undefined);
|
||||
|
||||
const result = acquireDreamLock(STATE_DIR);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify it wrote a lock file with wx flag
|
||||
const lockWrite = mockWriteText.mock.calls.find((c: unknown[]) =>
|
||||
(c[0] as string).endsWith("dream.lock"),
|
||||
);
|
||||
expect(lockWrite).toBeDefined();
|
||||
const lockData = JSON.parse(lockWrite![1] as string);
|
||||
expect(lockData.pid).toBe(process.pid);
|
||||
expect(lockData.startedAt).toBe(now);
|
||||
expect(lockWrite![2]).toEqual({ flag: "wx" });
|
||||
});
|
||||
|
||||
it("fails when lock exists and is fresh", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// Lock was created 10 minutes ago — still fresh (< 1 hour)
|
||||
mockReadText.mockImplementation((filePath: string) => {
|
||||
if (filePath.endsWith("dream.lock")) {
|
||||
return JSON.stringify({
|
||||
pid: 12345,
|
||||
startedAt: now - 10 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
|
||||
const result = acquireDreamLock(STATE_DIR);
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Should NOT have written a new lock
|
||||
const lockWrites = mockWriteText.mock.calls.filter((c: unknown[]) =>
|
||||
(c[0] as string).endsWith("dream.lock"),
|
||||
);
|
||||
expect(lockWrites).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("succeeds when lock is stale (>1hr old)", () => {
|
||||
const now = Date.now();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// Lock was created 2 hours ago — stale
|
||||
mockReadText.mockImplementation((filePath: string) => {
|
||||
if (filePath.endsWith("dream.lock")) {
|
||||
return JSON.stringify({
|
||||
pid: 99999,
|
||||
startedAt: now - 2 * 60 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
mockWriteText.mockReturnValue(undefined);
|
||||
|
||||
const result = acquireDreamLock(STATE_DIR);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Should have unlinked the stale lock
|
||||
expect(mockUnlink).toHaveBeenCalled();
|
||||
|
||||
// Should have written a new lock
|
||||
const lockWrite = mockWriteText.mock.calls.find((c: unknown[]) =>
|
||||
(c[0] as string).endsWith("dream.lock"),
|
||||
);
|
||||
expect(lockWrite).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// releaseDreamLock
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("releaseDreamLock", () => {
|
||||
it("removes lock file", () => {
|
||||
releaseDreamLock(STATE_DIR);
|
||||
expect(mockUnlink).toHaveBeenCalledWith(
|
||||
expect.stringContaining("dream.lock"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// recordDreamCompletion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("recordDreamCompletion", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("resets session counter and records timestamp", () => {
|
||||
const now = 1700000000000;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
setDreamState({
|
||||
lastConsolidatedAt: 0,
|
||||
sessionsSince: 15,
|
||||
lastSessionId: "session-xyz",
|
||||
});
|
||||
|
||||
recordDreamCompletion(STATE_DIR);
|
||||
|
||||
const written = getWrittenState();
|
||||
expect(written.lastConsolidatedAt).toBe(now);
|
||||
expect(written.sessionsSince).toBe(0);
|
||||
expect(written.lastSessionId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getDreamState
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getDreamState", () => {
|
||||
it("returns default state when no file exists", () => {
|
||||
setNoState();
|
||||
|
||||
const state = getDreamState(STATE_DIR);
|
||||
expect(state).toEqual({
|
||||
lastConsolidatedAt: 0,
|
||||
sessionsSince: 0,
|
||||
lastSessionId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns persisted state when file exists", () => {
|
||||
const persisted = {
|
||||
lastConsolidatedAt: 1700000000000,
|
||||
sessionsSince: 7,
|
||||
lastSessionId: "session-abc",
|
||||
};
|
||||
setDreamState(persisted);
|
||||
|
||||
const state = getDreamState(STATE_DIR);
|
||||
expect(state).toEqual(persisted);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Tests for providers.ts — providerToBackend adapter layer.
|
||||
*
|
||||
* Verifies that the Backend wrapper correctly delegates to the
|
||||
* underlying Mem0Provider methods with proper argument mapping.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { providerToBackend } from "../providers.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock provider factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMockProvider() {
|
||||
return {
|
||||
search: vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: "m1", memory: "found", score: 0.9 }]),
|
||||
add: vi.fn().mockResolvedValue({
|
||||
results: [{ id: "m1", event: "ADD", memory: "stored" }],
|
||||
}),
|
||||
get: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ id: "m1", memory: "test", created_at: "2026-01-01" }),
|
||||
getAll: vi.fn().mockResolvedValue([{ id: "m1", memory: "listed" }]),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAll: vi.fn().mockResolvedValue(undefined),
|
||||
history: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_USER = "test-user";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// search
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — search", () => {
|
||||
it("delegates to provider.search with correct options", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const results = await backend.search("hello world", {
|
||||
topK: 10,
|
||||
threshold: 0.5,
|
||||
keyword: true,
|
||||
rerank: true,
|
||||
filters: { category: "preference" },
|
||||
});
|
||||
|
||||
expect(provider.search).toHaveBeenCalledWith("hello world", {
|
||||
user_id: DEFAULT_USER,
|
||||
top_k: 10,
|
||||
threshold: 0.5,
|
||||
keyword_search: true,
|
||||
reranking: true,
|
||||
filters: { category: "preference" },
|
||||
});
|
||||
expect(results).toHaveLength(1);
|
||||
expect((results[0] as any).id).toBe("m1");
|
||||
});
|
||||
|
||||
it("uses default userId when opts.userId is not provided", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
await backend.search("query");
|
||||
|
||||
expect(provider.search).toHaveBeenCalledWith("query", {
|
||||
user_id: DEFAULT_USER,
|
||||
top_k: undefined,
|
||||
threshold: undefined,
|
||||
keyword_search: undefined,
|
||||
reranking: undefined,
|
||||
filters: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// add
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — add", () => {
|
||||
it("delegates to provider.add with content as message", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.add("Remember this fact");
|
||||
|
||||
expect(provider.add).toHaveBeenCalledWith(
|
||||
[{ role: "user", content: "Remember this fact" }],
|
||||
expect.objectContaining({ user_id: DEFAULT_USER }),
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("passes messages array when provided", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const messages = [
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "assistant", content: "Hello" },
|
||||
];
|
||||
await backend.add(undefined, messages);
|
||||
|
||||
expect(provider.add).toHaveBeenCalledWith(
|
||||
messages,
|
||||
expect.objectContaining({ user_id: DEFAULT_USER }),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards optional add options", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
await backend.add("fact", undefined, {
|
||||
runId: "run-1",
|
||||
metadata: { source: "test" },
|
||||
immutable: true,
|
||||
infer: false,
|
||||
expires: "2027-01-01",
|
||||
enableGraph: true,
|
||||
});
|
||||
|
||||
expect(provider.add).toHaveBeenCalledWith(
|
||||
[{ role: "user", content: "fact" }],
|
||||
expect.objectContaining({
|
||||
user_id: DEFAULT_USER,
|
||||
run_id: "run-1",
|
||||
metadata: { source: "test" },
|
||||
immutable: true,
|
||||
infer: false,
|
||||
expiration_date: "2027-01-01",
|
||||
enable_graph: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// get
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — get", () => {
|
||||
it("delegates to provider.get", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.get("mem-123");
|
||||
|
||||
expect(provider.get).toHaveBeenCalledWith("mem-123");
|
||||
expect((result as any).id).toBe("m1");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// listMemories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — listMemories", () => {
|
||||
it("delegates to provider.getAll", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const results = await backend.listMemories({ pageSize: 50 });
|
||||
|
||||
expect(provider.getAll).toHaveBeenCalledWith({
|
||||
user_id: DEFAULT_USER,
|
||||
page_size: 50,
|
||||
});
|
||||
expect(results).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses default userId when opts.userId is not provided", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
await backend.listMemories();
|
||||
|
||||
expect(provider.getAll).toHaveBeenCalledWith({
|
||||
user_id: DEFAULT_USER,
|
||||
page_size: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — update", () => {
|
||||
it("calls provider.update with content", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.update("mem-123", "updated text");
|
||||
|
||||
expect(provider.update).toHaveBeenCalledWith("mem-123", "updated text");
|
||||
expect((result as any).id).toBe("mem-123");
|
||||
expect((result as any).updated).toBe(true);
|
||||
});
|
||||
|
||||
it("warns and skips when only metadata is provided (no content)", async () => {
|
||||
const provider = createMockProvider();
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.update("mem-123", undefined, {
|
||||
tag: "important",
|
||||
});
|
||||
|
||||
expect(provider.update).not.toHaveBeenCalled();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("metadata updates are not supported"),
|
||||
);
|
||||
expect((result as any).id).toBe("mem-123");
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// delete
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — delete", () => {
|
||||
it("calls provider.delete with memoryId", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.delete("mem-456");
|
||||
|
||||
expect(provider.delete).toHaveBeenCalledWith("mem-456");
|
||||
expect((result as any).deleted).toBe("mem-456");
|
||||
});
|
||||
|
||||
it("calls provider.deleteAll when opts.all is true", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.delete(undefined, {
|
||||
all: true,
|
||||
userId: "custom-user",
|
||||
});
|
||||
|
||||
expect(provider.deleteAll).toHaveBeenCalledWith("custom-user");
|
||||
expect((result as any).deleted).toBe("all");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deleteEntities (platform-only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — deleteEntities", () => {
|
||||
it("throws platform-only error", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
await expect(
|
||||
backend.deleteEntities({ userId: DEFAULT_USER }),
|
||||
).rejects.toThrow("platform mode");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// status
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("providerToBackend — status", () => {
|
||||
it("returns connected: true", async () => {
|
||||
const provider = createMockProvider();
|
||||
const backend = providerToBackend(provider as any, DEFAULT_USER);
|
||||
|
||||
const result = await backend.status();
|
||||
|
||||
expect((result as any).connected).toBe(true);
|
||||
expect((result as any).backend).toBe("oss");
|
||||
});
|
||||
});
|
||||
+269
-87
@@ -7,30 +7,26 @@
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import type { ToolContext } from "../tools/index.ts";
|
||||
import type { ToolDeps } from "../tools/index.ts";
|
||||
import { registerAllTools } from "../tools/index.ts";
|
||||
import { createMemorySearchTool } from "../tools/memory-search.ts";
|
||||
import { createMemoryStoreTool } from "../tools/memory-store.ts";
|
||||
import { createMemoryAddTool } from "../tools/memory-add.ts";
|
||||
import { createMemoryGetTool } from "../tools/memory-get.ts";
|
||||
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
|
||||
import { createMemoryListTool } from "../tools/memory-list.ts";
|
||||
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
|
||||
import { createMemoryHistoryTool } from "../tools/memory-history.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMockToolContext(overrides = {}): ToolContext {
|
||||
function createMockToolDeps(overrides = {}): ToolDeps {
|
||||
return {
|
||||
api: {
|
||||
registerTool: vi.fn(),
|
||||
logger: { info: vi.fn(), warn: vi.fn() },
|
||||
} as any,
|
||||
backend: {
|
||||
get: vi.fn().mockResolvedValue({ id: "test-id", memory: "test memory" }),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
status: vi.fn().mockResolvedValue({ connected: true }),
|
||||
deleteEntities: vi.fn().mockResolvedValue(undefined),
|
||||
} as any,
|
||||
cfg: {
|
||||
mode: "platform",
|
||||
userId: "testuser",
|
||||
@@ -68,6 +64,7 @@ function createMockToolContext(overrides = {}): ToolContext {
|
||||
agentUserId: vi.fn().mockReturnValue("testuser:agent:test"),
|
||||
getCurrentSessionId: vi.fn().mockReturnValue(undefined),
|
||||
skillsActive: false,
|
||||
captureToolEvent: vi.fn(),
|
||||
buildAddOptions: vi
|
||||
.fn()
|
||||
.mockReturnValue({ user_id: "testuser", source: "OPENCLAW" }),
|
||||
@@ -84,22 +81,24 @@ function createMockToolContext(overrides = {}): ToolContext {
|
||||
|
||||
describe("registerAllTools", () => {
|
||||
it("calls api.registerTool exactly 7 times", () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
registerAllTools(ctx);
|
||||
expect(ctx.api.registerTool).toHaveBeenCalledTimes(7);
|
||||
});
|
||||
|
||||
it("registers tools with the correct names", () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
registerAllTools(ctx);
|
||||
|
||||
// Tools are registered as required (single argument — no metadata object).
|
||||
// The name comes from the tool definition itself (call[0]).
|
||||
const names = (
|
||||
ctx.api.registerTool as ReturnType<typeof vi.fn>
|
||||
).mock.calls.map((call: unknown[]) => (call[1] as { name: string }).name);
|
||||
).mock.calls.map((call: unknown[]) => (call[0] as { name: string }).name);
|
||||
|
||||
expect(names).toEqual([
|
||||
"memory_search",
|
||||
"memory_store",
|
||||
"memory_add",
|
||||
"memory_get",
|
||||
"memory_list",
|
||||
"memory_update",
|
||||
@@ -107,6 +106,16 @@ describe("registerAllTools", () => {
|
||||
"memory_history",
|
||||
]);
|
||||
});
|
||||
|
||||
it("registers tools without a second argument (required, not optional)", () => {
|
||||
const ctx = createMockToolDeps();
|
||||
registerAllTools(ctx);
|
||||
|
||||
const calls = (ctx.api.registerTool as ReturnType<typeof vi.fn>).mock.calls;
|
||||
for (const call of calls) {
|
||||
expect(call).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -116,7 +125,7 @@ describe("registerAllTools", () => {
|
||||
describe("tool factory shape", () => {
|
||||
const factories = [
|
||||
{ fn: createMemorySearchTool, expectedName: "memory_search" },
|
||||
{ fn: createMemoryStoreTool, expectedName: "memory_store" },
|
||||
{ fn: createMemoryAddTool, expectedName: "memory_add" },
|
||||
{ fn: createMemoryGetTool, expectedName: "memory_get" },
|
||||
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
|
||||
{ fn: createMemoryListTool, expectedName: "memory_list" },
|
||||
@@ -125,7 +134,7 @@ describe("tool factory shape", () => {
|
||||
for (const { fn, expectedName } of factories) {
|
||||
describe(expectedName, () => {
|
||||
it("returns an object with name, label, description, parameters, and execute", () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = fn(ctx);
|
||||
|
||||
expect(tool.name).toBe(expectedName);
|
||||
@@ -146,7 +155,7 @@ describe("tool factory shape", () => {
|
||||
|
||||
describe("memory_search execute", () => {
|
||||
it("returns formatted results when provider returns matches", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemorySearchTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", {
|
||||
@@ -163,7 +172,7 @@ describe("memory_search execute", () => {
|
||||
});
|
||||
|
||||
it("returns 'no relevant memories' when provider returns empty", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn().mockResolvedValue([]),
|
||||
add: vi.fn(),
|
||||
@@ -183,7 +192,7 @@ describe("memory_search execute", () => {
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn().mockRejectedValue(new Error("network failure")),
|
||||
add: vi.fn(),
|
||||
@@ -204,7 +213,7 @@ describe("memory_search execute", () => {
|
||||
});
|
||||
|
||||
it("calls resolveUserId with provided agentId and userId", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemorySearchTool(ctx);
|
||||
|
||||
await tool.execute("call-4", {
|
||||
@@ -220,7 +229,7 @@ describe("memory_search execute", () => {
|
||||
});
|
||||
|
||||
it("passes limit to buildSearchOptions", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemorySearchTool(ctx);
|
||||
|
||||
await tool.execute("call-5", { query: "test", limit: 10 });
|
||||
@@ -232,7 +241,7 @@ describe("memory_search execute", () => {
|
||||
const searchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: "s1", memory: "session mem", score: 0.8 }]);
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi.fn().mockReturnValue("session-abc"),
|
||||
provider: {
|
||||
search: searchMock,
|
||||
@@ -273,7 +282,7 @@ describe("memory_search execute", () => {
|
||||
{ id: "m2", memory: "session only", score: 0.7 },
|
||||
]);
|
||||
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi.fn().mockReturnValue("session-xyz"),
|
||||
provider: {
|
||||
search: searchMock,
|
||||
@@ -300,13 +309,13 @@ describe("memory_search execute", () => {
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// memory_store execute
|
||||
// memory_add execute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("memory_store execute", () => {
|
||||
describe("memory_add execute", () => {
|
||||
it("calls provider.add with the text and returns stored result", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", {
|
||||
text: "User prefers dark mode",
|
||||
@@ -323,8 +332,8 @@ describe("memory_store execute", () => {
|
||||
});
|
||||
|
||||
it("returns error when no text or facts are provided", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-2", {});
|
||||
|
||||
@@ -333,8 +342,8 @@ describe("memory_store execute", () => {
|
||||
});
|
||||
|
||||
it("supports facts array", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-3", {
|
||||
facts: ["fact one", "fact two"],
|
||||
@@ -350,7 +359,7 @@ describe("memory_store execute", () => {
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn().mockResolvedValue([]),
|
||||
add: vi.fn().mockRejectedValue(new Error("API error")),
|
||||
@@ -361,11 +370,11 @@ describe("memory_store execute", () => {
|
||||
history: vi.fn(),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-4", { text: "test" });
|
||||
|
||||
expect(result.content[0].text).toContain("Memory store failed");
|
||||
expect(result.content[0].text).toContain("Memory add failed");
|
||||
expect(result.details.error).toContain("API error");
|
||||
});
|
||||
|
||||
@@ -373,7 +382,7 @@ describe("memory_store execute", () => {
|
||||
const addMock = vi.fn().mockResolvedValue({
|
||||
results: [{ event: "ADD", memory: "stored in skills mode" }],
|
||||
});
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
skillsActive: true,
|
||||
provider: {
|
||||
search: vi.fn().mockResolvedValue([]),
|
||||
@@ -385,7 +394,7 @@ describe("memory_store execute", () => {
|
||||
history: vi.fn(),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-5", {
|
||||
text: "skills fact",
|
||||
@@ -395,18 +404,17 @@ describe("memory_store execute", () => {
|
||||
expect(addMock).toHaveBeenCalledOnce();
|
||||
const addOpts = addMock.mock.calls[0][1];
|
||||
expect(addOpts.infer).toBe(false);
|
||||
expect(addOpts.source).toBe("OPENCLAW");
|
||||
expect(result.details.mode).toBe("skills");
|
||||
expect(result.details.category).toBe("preference");
|
||||
});
|
||||
|
||||
it("blocks subagent sessions from storing", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi
|
||||
.fn()
|
||||
.mockReturnValue("agent:main:subagent:uuid-123"),
|
||||
});
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-6", { text: "subagent fact" });
|
||||
|
||||
@@ -419,7 +427,7 @@ describe("memory_store execute", () => {
|
||||
const addMock = vi.fn().mockResolvedValue({
|
||||
results: [{ event: "ADD", memory: "stored" }],
|
||||
});
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
skillsActive: false,
|
||||
provider: {
|
||||
search: searchMock,
|
||||
@@ -431,7 +439,7 @@ describe("memory_store execute", () => {
|
||||
history: vi.fn(),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryStoreTool(ctx);
|
||||
const tool = createMemoryAddTool(ctx);
|
||||
|
||||
await tool.execute("call-7", { text: "new fact" });
|
||||
|
||||
@@ -447,7 +455,7 @@ describe("memory_store execute", () => {
|
||||
|
||||
describe("memory_get execute", () => {
|
||||
it("calls provider.get with the memoryId and returns formatted result", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryGetTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", { memoryId: "test-id" });
|
||||
@@ -461,7 +469,7 @@ describe("memory_get execute", () => {
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
@@ -486,11 +494,11 @@ describe("memory_get execute", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("memory_delete execute", () => {
|
||||
it("deletes by memory_id via provider.delete", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
it("deletes by memoryId via provider.delete", async () => {
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", { memory_id: "mem-abc" });
|
||||
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
|
||||
|
||||
expect(ctx.provider!.delete).toHaveBeenCalledWith("mem-abc");
|
||||
expect(result.content[0].text).toBe("Memory mem-abc deleted.");
|
||||
@@ -503,7 +511,7 @@ describe("memory_delete execute", () => {
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: "m1", memory: "match", score: 0.95 }]);
|
||||
const deleteMock = vi.fn().mockResolvedValue(undefined);
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: searchMock,
|
||||
add: vi.fn(),
|
||||
@@ -532,7 +540,7 @@ describe("memory_delete execute", () => {
|
||||
{ id: "m2", memory: "candidate two", score: 0.6 },
|
||||
]);
|
||||
const deleteMock = vi.fn();
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: searchMock,
|
||||
add: vi.fn(),
|
||||
@@ -557,7 +565,7 @@ describe("memory_delete execute", () => {
|
||||
});
|
||||
|
||||
it("returns no matching memories when query yields empty results", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn().mockResolvedValue([]),
|
||||
add: vi.fn(),
|
||||
@@ -577,7 +585,7 @@ describe("memory_delete execute", () => {
|
||||
});
|
||||
|
||||
it("requires confirm:true for bulk delete (all)", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-5", { all: true });
|
||||
@@ -588,7 +596,7 @@ describe("memory_delete execute", () => {
|
||||
|
||||
it("performs bulk delete when all:true and confirm:true", async () => {
|
||||
const deleteAllMock = vi.fn().mockResolvedValue(undefined);
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
@@ -612,59 +620,34 @@ describe("memory_delete execute", () => {
|
||||
expect(result.details.action).toBe("deleted_all");
|
||||
});
|
||||
|
||||
it("requires confirm:true for entity cascade delete", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-7", { entity: true });
|
||||
|
||||
expect(result.details.error).toBe("confirmation_required");
|
||||
});
|
||||
|
||||
it("performs entity cascade delete when entity:true and confirm:true", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-8", {
|
||||
entity: true,
|
||||
confirm: true,
|
||||
user_id: "alice",
|
||||
});
|
||||
|
||||
expect(ctx.backend.deleteEntities).toHaveBeenCalledWith({
|
||||
userId: "alice",
|
||||
});
|
||||
expect(result.details.action).toBe("entity_deleted");
|
||||
});
|
||||
|
||||
it("returns error when no mode param is specified", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-9", {});
|
||||
|
||||
expect(result.content[0].text).toContain(
|
||||
"Provide memory_id, query, all, or entity",
|
||||
"Provide memoryId, query, or all:true",
|
||||
);
|
||||
expect(result.details.error).toBe("missing_param");
|
||||
});
|
||||
|
||||
it("blocks subagent sessions from deleting", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi
|
||||
.fn()
|
||||
.mockReturnValue("agent:main:subagent:uuid-456"),
|
||||
});
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-10", { memory_id: "m1" });
|
||||
const result = await tool.execute("call-10", { memoryId: "m1" });
|
||||
|
||||
expect(ctx.provider!.delete).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe("subagent_blocked");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
@@ -677,7 +660,7 @@ describe("memory_delete execute", () => {
|
||||
});
|
||||
const tool = createMemoryDeleteTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-11", { memory_id: "m1" });
|
||||
const result = await tool.execute("call-11", { memoryId: "m1" });
|
||||
|
||||
expect(result.content[0].text).toContain("Memory delete failed");
|
||||
expect(result.details.error).toContain("delete failed");
|
||||
@@ -690,7 +673,7 @@ describe("memory_delete execute", () => {
|
||||
|
||||
describe("memory_list execute", () => {
|
||||
it("calls provider.getAll and returns formatted list", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryListTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", {});
|
||||
@@ -703,7 +686,7 @@ describe("memory_list execute", () => {
|
||||
});
|
||||
|
||||
it("returns 'no memories stored' when provider returns empty", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
@@ -723,7 +706,7 @@ describe("memory_list execute", () => {
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
@@ -743,7 +726,7 @@ describe("memory_list execute", () => {
|
||||
});
|
||||
|
||||
it("resolves userId from agentId", async () => {
|
||||
const ctx = createMockToolContext();
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryListTool(ctx);
|
||||
|
||||
await tool.execute("call-4", { agentId: "researcher" });
|
||||
@@ -765,7 +748,7 @@ describe("memory_list execute", () => {
|
||||
{ id: "m2", memory: "session only" },
|
||||
]);
|
||||
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi.fn().mockReturnValue("session-123"),
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
@@ -790,7 +773,7 @@ describe("memory_list execute", () => {
|
||||
const getAllMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: "s1", memory: "session mem" }]);
|
||||
const ctx = createMockToolContext({
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi.fn().mockReturnValue("sess-abc"),
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
@@ -813,3 +796,202 @@ describe("memory_list execute", () => {
|
||||
expect(result.details.count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// memory_update execute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("memory_update execute", () => {
|
||||
it("calls provider.update and returns success", async () => {
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryUpdateTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", {
|
||||
memoryId: "mem-123",
|
||||
text: "Updated preference",
|
||||
});
|
||||
|
||||
expect(ctx.provider!.update).toHaveBeenCalledWith(
|
||||
"mem-123",
|
||||
"Updated preference",
|
||||
);
|
||||
expect(result.content[0].text).toContain("Updated memory mem-123");
|
||||
expect(result.content[0].text).toContain("Updated preference");
|
||||
expect(result.details.action).toBe("updated");
|
||||
expect(result.details.id).toBe("mem-123");
|
||||
});
|
||||
|
||||
it("truncates long text in response", async () => {
|
||||
const ctx = createMockToolDeps();
|
||||
const tool = createMemoryUpdateTool(ctx);
|
||||
|
||||
const longText = "A".repeat(120);
|
||||
const result = await tool.execute("call-2", {
|
||||
memoryId: "mem-456",
|
||||
text: longText,
|
||||
});
|
||||
|
||||
expect(ctx.provider!.update).toHaveBeenCalledWith("mem-456", longText);
|
||||
// The response text should contain the first 80 chars followed by "..."
|
||||
expect(result.content[0].text).toContain("A".repeat(80) + "...");
|
||||
expect(result.content[0].text).not.toContain("A".repeat(81));
|
||||
expect(result.details.action).toBe("updated");
|
||||
});
|
||||
|
||||
it("blocks subagent sessions", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
getCurrentSessionId: vi
|
||||
.fn()
|
||||
.mockReturnValue("agent:main:subagent:uuid-789"),
|
||||
});
|
||||
const tool = createMemoryUpdateTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-3", {
|
||||
memoryId: "mem-123",
|
||||
text: "should not update",
|
||||
});
|
||||
|
||||
expect(ctx.provider!.update).not.toHaveBeenCalled();
|
||||
expect(result.content[0].text).toContain(
|
||||
"not available in subagent sessions",
|
||||
);
|
||||
expect(result.details.error).toBe("subagent_blocked");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn().mockRejectedValue(new Error("update conflict")),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn(),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryUpdateTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-4", {
|
||||
memoryId: "mem-123",
|
||||
text: "new text",
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toContain("Memory update failed");
|
||||
expect(result.content[0].text).toContain("update conflict");
|
||||
expect(result.details.error).toContain("update conflict");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// memory_history execute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("memory_history execute", () => {
|
||||
it("returns formatted history", async () => {
|
||||
const historyEntries = [
|
||||
{
|
||||
event: "ADD",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
old_memory: null,
|
||||
new_memory: "Initial memory text",
|
||||
},
|
||||
{
|
||||
event: "UPDATE",
|
||||
created_at: "2026-01-02T00:00:00Z",
|
||||
old_memory: "Initial memory text",
|
||||
new_memory: "Updated memory text",
|
||||
},
|
||||
];
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue(historyEntries),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
|
||||
|
||||
expect(ctx.provider!.history).toHaveBeenCalledWith("mem-abc");
|
||||
expect(result.content[0].text).toContain("History for memory mem-abc");
|
||||
expect(result.content[0].text).toContain("2 entries");
|
||||
expect(result.content[0].text).toContain("[ADD]");
|
||||
expect(result.content[0].text).toContain("[UPDATE]");
|
||||
expect(result.content[0].text).toContain("Initial memory text");
|
||||
expect(result.content[0].text).toContain("Updated memory text");
|
||||
expect(result.content[0].text).toContain("Old: (none)");
|
||||
expect(result.details.count).toBe(2);
|
||||
expect(result.details.history).toEqual(historyEntries);
|
||||
});
|
||||
|
||||
it("handles empty history", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-2", { memoryId: "mem-empty" });
|
||||
|
||||
expect(result.content[0].text).toBe(
|
||||
"No history found for memory mem-empty.",
|
||||
);
|
||||
expect(result.details.count).toBe(0);
|
||||
});
|
||||
|
||||
it("handles null history", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-3", { memoryId: "mem-null" });
|
||||
|
||||
expect(result.content[0].text).toBe(
|
||||
"No history found for memory mem-null.",
|
||||
);
|
||||
expect(result.details.count).toBe(0);
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockRejectedValue(new Error("history unavailable")),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-4", { memoryId: "mem-err" });
|
||||
|
||||
expect(result.content[0].text).toContain("Memory history failed");
|
||||
expect(result.content[0].text).toContain("history unavailable");
|
||||
expect(result.details.error).toContain("history unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user