chore: merge origin/main into mintlify/af82bda4 and resolve conflicts

This commit is contained in:
mintlify[bot]
2026-09-16 11:46:52 +00:00
committed by GitHub
parent b5ae772eae
commit 08d9636491
693 changed files with 55668 additions and 22676 deletions
+14 -108
View File
@@ -29,11 +29,6 @@ vi.mock("../fs-safe.ts", () => ({
unlink: vi.fn(),
}));
vi.mock("../skill-loader.ts", () => ({
loadDreamPrompt: vi.fn().mockReturnValue("dream prompt"),
}));
// ---------------------------------------------------------------------------
// Imports (after mocks)
// ---------------------------------------------------------------------------
@@ -46,7 +41,6 @@ import {
enableSkillsConfig,
getBaseUrl,
} from "../cli/config-file.ts";
import { loadDreamPrompt } from "../skill-loader.ts";
// ---------------------------------------------------------------------------
// Mock Commander program builder
@@ -263,8 +257,6 @@ describe("registerCliCommands", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValue({});
(writePluginAuth as ReturnType<typeof vi.fn>).mockImplementation(() => {});
(getBaseUrl as ReturnType<typeof vi.fn>).mockReturnValue("https://api.mem0.ai");
(loadDreamPrompt as ReturnType<typeof vi.fn>).mockReturnValue("dream prompt");
consoleSpy = {
log: vi.spyOn(console, "log").mockImplementation(() => {}),
error: vi.spyOn(console, "error").mockImplementation(() => {}),
@@ -311,7 +303,6 @@ describe("registerCliCommands", () => {
expect(names).toContain("delete");
expect(names).toContain("status");
expect(names).toContain("config");
expect(names).toContain("dream");
});
it("registers config subcommands: show, get, set", () => {
@@ -1037,6 +1028,19 @@ describe("registerCliCommands", () => {
// ========================================================================
describe("status subcommand", () => {
it("reports setup instructions when Mem0 is not configured", async () => {
const { mem0, backend, cfg } = setup();
(cfg as any).needsSetup = true;
const statusCmd = findCommand(mem0, "status")!;
await statusCmd._action!();
expect(backend.status).not.toHaveBeenCalled();
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("openclaw mem0 init"),
);
});
it("calls backend.status and prints connection info", async () => {
const { mem0, backend } = setup();
const statusCmd = findCommand(mem0, "status")!;
@@ -1280,104 +1284,6 @@ describe("registerCliCommands", () => {
});
});
// ========================================================================
// dream subcommand
// ========================================================================
describe("dream subcommand", () => {
it("fetches memories and outputs dream prompt to stdout", async () => {
const { mem0, provider } = setup();
provider.getAll.mockResolvedValueOnce([
{
id: "m1",
memory: "User is an engineer",
categories: ["identity"],
metadata: { category: "identity", importance: 0.9 },
created_at: "2026-01-01",
},
]);
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const dreamCmd = findCommand(mem0, "dream")!;
await dreamCmd._action!({});
expect(provider.getAll).toHaveBeenCalledWith(
expect.objectContaining({
user_id: "testuser",
source: "OPENCLAW",
}),
);
expect(loadDreamPrompt).toHaveBeenCalled();
// stdout should contain the dream prompt
const stdoutOutput = stdoutSpy.mock.calls.map((c) => c[0]).join("");
expect(stdoutOutput).toContain("<dream-protocol>");
expect(stdoutOutput).toContain("dream prompt");
expect(stdoutOutput).toContain("<all-memories");
expect(stdoutOutput).toContain("User is an engineer");
stdoutSpy.mockRestore();
});
it("prints dry-run message and does not output dream prompt", async () => {
const { mem0, provider } = setup();
provider.getAll.mockResolvedValueOnce([
{ id: "m1", memory: "test", categories: [], metadata: {}, created_at: "2026-01-01" },
]);
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const dreamCmd = findCommand(mem0, "dream")!;
await dreamCmd._action!({ dryRun: true });
// Dry run should write inventory to stderr, NOT dream prompt to stdout
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining("Dry run"),
);
expect(stdoutSpy).not.toHaveBeenCalled();
stdoutSpy.mockRestore();
});
it("prints message when no memories to consolidate", async () => {
const { mem0, provider } = setup();
provider.getAll.mockResolvedValueOnce([]);
const dreamCmd = findCommand(mem0, "dream")!;
await dreamCmd._action!({});
expect(consoleSpy.log).toHaveBeenCalledWith(
"No memories to consolidate.",
);
});
it("prints error when dream skill file is not found", async () => {
const { mem0, provider } = setup();
provider.getAll.mockResolvedValueOnce([
{ id: "m1", memory: "test", categories: [], metadata: {}, created_at: "2026-01-01" },
]);
(loadDreamPrompt as ReturnType<typeof vi.fn>).mockReturnValueOnce("");
const dreamCmd = findCommand(mem0, "dream")!;
await dreamCmd._action!({});
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining("Dream skill file not found"),
);
});
it("handles dream errors gracefully", async () => {
const { mem0, provider } = setup();
provider.getAll.mockRejectedValueOnce(new Error("dream boom"));
const dreamCmd = findCommand(mem0, "dream")!;
await dreamCmd._action!({});
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Dream failed"),
);
});
});
// ========================================================================
// import subcommand
// ========================================================================
@@ -1647,7 +1553,7 @@ describe("registerCliCommands", () => {
// ========================================================================
describe("--json flag registration", () => {
for (const name of ["search", "add", "get", "list", "update", "delete", "status", "import", "dream"]) {
for (const name of ["search", "add", "get", "list", "update", "delete", "status", "import"]) {
it(`registers --json on ${name}`, () => {
const { mem0 } = setup();
const cmd = findCommand(mem0, name)!;
+10 -7
View File
@@ -1,6 +1,8 @@
/**
* Tests for config.ts — mem0ConfigSchema.parse() and exported constants.
*/
import { readFileSync } from "node:fs";
import { describe, it, expect } from "vitest";
import {
mem0ConfigSchema,
@@ -8,6 +10,14 @@ import {
DEFAULT_CUSTOM_CATEGORIES,
} from "../config.ts";
describe("plugin manifest", () => {
it("matches the package version", () => {
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const manifest = JSON.parse(readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
expect(manifest.version).toBe(packageJson.version);
});
});
// ---------------------------------------------------------------------------
// Exported constants
// ---------------------------------------------------------------------------
@@ -379,13 +389,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => {
tokenBudget: 2000,
maxMemories: 10,
},
dream: {
enabled: true,
auto: true,
minHours: 12,
minSessions: 3,
minMemories: 15,
},
domain: "engineering",
customRules: {
include: ["tool configs"],
@@ -1,376 +0,0 @@
/**
* 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);
});
});