/** * PR-C Deprecation Warning System — Tests * * Run: node --test test/architecture/deprecation-warning.test.mjs */ import { describe, it, beforeEach } from "node:test"; import assert from "node:assert"; import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { warnOnce, resetWarnings, getWarningCount, warnMany, runDeprecationCheck } from "../../scripts/lib/deprecation-warning.mjs"; const CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json"); const TEST_DIR = join(homedir(), ".openclaw", "workspace", "test", "architecture"); const TMP_CONFIG = join(homedir(), ".openclaw", "openclaw-prc-test.json"); beforeEach(() => { resetWarnings(); }); describe("PR-C: Deprecation Warning System", () => { // ─── warnOnce utility tests ────────────────────────────── describe("warnOnce utility", () => { it("warn-01: same key only warns once", () => { const r1 = warnOnce("test.key1", "first message"); const r2 = warnOnce("test.key1", "second message"); const r3 = warnOnce("test.key1", "third message"); assert.equal(r1, true, "first call must emit"); assert.equal(r2, false, "second call must be suppressed"); assert.equal(r3, false, "third call must be suppressed"); assert.equal(getWarningCount(), 1, "only 1 unique key warned"); }); it("warn-02: different keys both warn", () => { const r1 = warnOnce("test.key1", "msg1"); const r2 = warnOnce("test.key2", "msg2"); assert.equal(r1, true); assert.equal(r2, true); assert.equal(getWarningCount(), 2); }); it("warn-03: force option bypasses deduplication", () => { warnOnce("test.force", "first"); const r = warnOnce("test.force", "second", { force: true }); assert.equal(r, true, "force must emit even if already warned"); assert.equal(getWarningCount(), 1, "key count unchanged (same key)"); }); it("warn-04: resetWarnings clears all state", () => { warnOnce("test.a", "a"); warnOnce("test.b", "b"); assert.equal(getWarningCount(), 2); resetWarnings(); assert.equal(getWarningCount(), 0); // Can warn again after reset const r = warnOnce("test.a", "a again"); assert.equal(r, true); assert.equal(getWarningCount(), 1); }); it("warn-05: warnMany deduplicates across batch", () => { const emitted = warnMany([ { key: "a", message: "msg a" }, { key: "b", message: "msg b" }, { key: "a", message: "msg a again" }, // duplicate ]); assert.equal(emitted.length, 2, "only 2 unique warnings emitted"); assert.equal(getWarningCount(), 2); }); it("warn-06: runDeprecationCheck catches errors", () => { const result = runDeprecationCheck("test-check", () => { return [{ key: "test.ok", message: "all good" }]; }); assert.equal(result.name, "test-check"); assert.equal(result.warnings.length, 1); const errResult = runDeprecationCheck("bad-check", () => { throw new Error("simulated failure"); }); assert.equal(errResult.warnings[0].key, "bad-check"); assert.ok(errResult.warnings[0].message.includes("simulated failure")); }); }); // ─── Config detection tests ───────────────────────────── describe("config detection", () => { it("detect-01: real config loads without crashing", () => { assert.ok(existsSync(CONFIG_PATH), "openclaw.json must exist"); const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); assert.ok(typeof config === "object"); assert.ok(config.gateway || config.agents, "config must have expected sections"); }); it("detect-02: active-memory blocking mode detected", () => { // Read the actual config to check current state const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); const activeCfg = config?.plugins?.entries?.["active-memory"]; if (activeCfg) { const mode = activeCfg.config?.mode; console.log(` [DETECT] active-memory mode: "${mode || "default"}"`); if (mode === "blocking") { const r = warnOnce("active-memory.blocking", "active-memory blocking mode is deprecated. Set mode to 'precompute'."); assert.equal(r, true); } } assert.ok(true, "check completed without crash"); }); it("detect-03: dreaming REM phase detected", () => { const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); const memoryCoreCfg = config?.plugins?.entries?.["memory-core"]; if (memoryCoreCfg?.config?.dreaming) { const dreaming = memoryCoreCfg.config.dreaming; const remEnabled = dreaming?.phases?.rem?.enabled; console.log(` [DETECT] dreaming REM enabled: ${remEnabled ?? "not configured"}`); if (remEnabled === true) { warnOnce("dreaming.rem-phase", "Dreaming REM phase is deprecated. Will be replaced by Collect/Promote."); } } assert.ok(true, "check completed without crash"); }); it("detect-04: memory-wiki plugin reference detectable", () => { const wikiPath = join( "/opt/homebrew/lib/node_modules/openclaw", "dist/extensions/memory-wiki/openclaw.plugin.json" ); const exists = existsSync(wikiPath); console.log(` [DETECT] memory-wiki plugin: ${exists ? "present" : "not found"}`); if (exists) { const plugin = JSON.parse(readFileSync(wikiPath, "utf8")); assert.equal(plugin.id, "memory-wiki"); warnOnce("memory-wiki.plugin", "memory-wiki plugin is legacy. Tools remain available during migration."); } assert.ok(true, "check completed without crash"); }); it("detect-05: QMD reference in config detectable", () => { const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); const qmdRefs = []; // Check memory search if (config?.agents?.defaults?.memorySearch?.provider === "qmd") { qmdRefs.push("memorySearch.provider=qmd"); } if (config?.agents?.defaults?.memorySearch?.backend === "qmd") { qmdRefs.push("memorySearch.backend=qmd"); } // Check active-memory const activeQmd = config?.plugins?.entries?.["active-memory"]?.config?.qmd; if (activeQmd?.searchMode) { qmdRefs.push("active-memory.qmd.searchMode"); } console.log(` [DETECT] QMD references found: ${qmdRefs.length}`); for (const ref of qmdRefs) { console.log(` • ${ref}`); warnOnce(`qmd.${ref}`, "QMD memory engine is legacy. Migrate to Builtin MemoryCore."); } assert.ok(true, "check completed without crash"); }); it("detect-06: Honcho/LanceDB plugins detectable", () => { const extDir = "/opt/homebrew/lib/node_modules/openclaw/dist/extensions"; const legacyPlugins = ["memory-honcho", "memory-lancedb"]; let found = 0; for (const name of legacyPlugins) { // Check if directory or plugin reference exists // These are NOT in the current install, which is expected (they're optional) const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); const enabled = config?.plugins?.entries?.[name]?.enabled; console.log(` [DETECT] ${name}: ${enabled === true ? "enabled" : "not enabled"}`); if (enabled === true) { warnOnce(`${name}.plugin`, `${name} memory plugin is legacy. Data readable during migration.`); found++; } } console.log(` [DETECT] legacy memory plugins enabled: ${found}`); assert.ok(true, "check completed without crash"); }); it("detect-07: commitments auto-infer detectable", () => { const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); const autoInfer = config?.commitments?.autoInfer; console.log(` [DETECT] commitments auto-infer: ${autoInfer ?? "not configured"}`); if (autoInfer === true) { warnOnce("commitments.auto-infer", "Commitments auto-infer is deprecated. Use explicit cron tasks."); } assert.ok(true, "check completed without crash"); }); }); // ─── Integration tests ────────────────────────────────── describe("integration", () => { it("integ-01: check-deprecations script runs without error", () => { const scriptPath = join(import.meta.dirname, "../../scripts/check-deprecations.mjs"); const result = spawnSync("node", [scriptPath], { encoding: "utf8", timeout: 15000, maxBuffer: 1024 * 1024, }); console.log(" [INTEG] check-deprecations exit code:", result.status); console.log(" [INTEG] check-deprecations stdout:", result.stdout?.slice(0, 400) || "(empty)"); if (result.stderr) { console.log(" [INTEG] stderr:", result.stderr.slice(0, 200)); } // Script should never crash — always exit 0 assert.equal(result.status, 0, "check-deprecations must exit 0 even with warnings"); }); it("integ-02: deprecation warnings do NOT cause process exit", () => { // warnOnce writes to console.warn — it should never throw assert.doesNotThrow(() => { warnOnce("test.integration", "This is a test deprecation warning"); }, "warnOnce must never throw"); assert.equal(getWarningCount(), 1); }); it("integ-03: all 8 deprecation categories checkable", () => { const categories = [ "active-memory.blocking", "dreaming.rem-phase", "dreaming.dream-diary", "qmd.reference", "honcho.plugin", "lancedb.plugin", "memory-wiki.plugin", "commitments.auto-infer", ]; // Each category should have a defined key pattern for (const cat of categories) { assert.ok(typeof cat === "string" && cat.length > 0, `category ${cat} must be a non-empty string`); } console.log(` [INTEG] ${categories.length} deprecation categories defined`); assert.equal(categories.length, 8, "must cover all 8 deprecation categories"); }); }); });