import type { OpenClawPluginCommandDefinition, PluginCommandContext, } from "openclaw/plugin-sdk/core"; import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core"; import { describe, expect, it, vi } from "vitest"; import { registerDreamingCommand } from "./dreaming-command.js"; function asRecord(value: unknown): Record | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } return value as Record; } function resolveStoredDreaming(config: OpenClawConfig): Record { const entry = asRecord(config.plugins?.entries?.["memory-core"]); const pluginConfig = asRecord(entry?.config); return asRecord(pluginConfig?.dreaming) ?? {}; } function createHarness(initialConfig: OpenClawConfig = {}) { let command: OpenClawPluginCommandDefinition | undefined; let runtimeConfig: OpenClawConfig = initialConfig; const runtime = { config: { loadConfig: vi.fn(() => runtimeConfig), writeConfigFile: vi.fn(async (nextConfig: OpenClawConfig) => { runtimeConfig = nextConfig; }), }, } as unknown as OpenClawPluginApi["runtime"]; const api = { runtime, registerCommand: vi.fn((definition: OpenClawPluginCommandDefinition) => { command = definition; }), } as unknown as OpenClawPluginApi; registerDreamingCommand(api); if (!command) { throw new Error("memory-core did not register /dreaming"); } return { command, runtime, getRuntimeConfig: () => runtimeConfig, }; } function createCommandContext(args?: string): PluginCommandContext { return { channel: "webchat", isAuthorizedSender: true, commandBody: args ? `/dreaming ${args}` : "/dreaming", args, config: {}, requestConversationBinding: async () => ({ status: "error", message: "unsupported" }), detachConversationBinding: async () => ({ removed: false }), getCurrentConversationBinding: async () => null, }; } describe("memory-core /dreaming command", () => { it("registers with an enable/disable description", () => { const { command } = createHarness(); expect(command.name).toBe("dreaming"); expect(command.acceptsArgs).toBe(true); expect(command.description).toContain("Enable or disable"); }); it("shows phase explanations when invoked without args", async () => { const { command } = createHarness(); const result = await command.handler(createCommandContext()); expect(result.text).toContain("Usage: /dreaming status"); expect(result.text).toContain("Dreaming status:"); expect(result.text).toContain("- implementation detail: each sweep runs light -> REM -> deep."); expect(result.text).toContain( "- deep is the only stage that writes durable entries to MEMORY.md.", ); }); it("persists global enablement under plugins.entries.memory-core.config.dreaming.enabled", async () => { const { command, runtime, getRuntimeConfig } = createHarness({ plugins: { entries: { "memory-core": { config: { dreaming: { phases: { deep: { minScore: 0.9, }, }, frequency: "0 */6 * * *", }, }, }, }, }, }); const result = await command.handler(createCommandContext("off")); expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1); expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({ enabled: false, frequency: "0 */6 * * *", }); expect(result.text).toContain("Dreaming disabled."); }); it("returns status without mutating config", async () => { const { command, runtime } = createHarness({ plugins: { entries: { "memory-core": { config: { dreaming: { frequency: "15 */8 * * *", }, }, }, }, }, agents: { defaults: { userTimezone: "America/Los_Angeles", }, }, }); const result = await command.handler(createCommandContext("status")); expect(result.text).toContain("Dreaming status:"); expect(result.text).toContain("- enabled: off (America/Los_Angeles)"); expect(result.text).toContain("- sweep cadence: 15 */8 * * *"); expect(result.text).toContain("- promotion policy: score>=0.8, recalls>=3, uniqueQueries>=3"); expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); }); it("shows usage for invalid args and does not mutate config", async () => { const { command, runtime } = createHarness(); const result = await command.handler(createCommandContext("unknown-mode")); expect(result.text).toContain("Usage: /dreaming status"); expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); }); });