mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-13 18:26:04 +00:00
Summary: - Merged fix(memory): record structured dreaming outcomes after ClawSweeper review. Automerge notes: - PR branch already contained follow-up commit before automerge: chore(plugin-sdk): update surface budgets Validation: - ClawSweeper review passed for head9fa7d20a96. - Required merge gates passed before the squash merge. Prepared head SHA:9fa7d20a96Review: https://github.com/openclaw/openclaw/pull/97723#issuecomment-4831902831 Co-authored-by: momothemage <niuzhengnan@163.com> Approved-by: momothemage
222 lines
7.4 KiB
TypeScript
222 lines
7.4 KiB
TypeScript
// Memory Core tests cover memory events plugin behavior.
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
readMemoryHostEventRecords,
|
|
readMemoryHostEvents,
|
|
resolveMemoryHostEventLogPath,
|
|
} from "openclaw/plugin-sdk/memory-host-events";
|
|
import { describe, expect, it } from "vitest";
|
|
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
|
|
import {
|
|
applyShortTermPromotions,
|
|
rankShortTermPromotionCandidates,
|
|
recordShortTermRecalls,
|
|
} from "./short-term-promotion.js";
|
|
import { createMemoryCoreTestHarness } from "./test-helpers.js";
|
|
|
|
const { createTempWorkspace } = createMemoryCoreTestHarness();
|
|
|
|
describe("memory host event journal integration", () => {
|
|
it("records recall and promotion events from short-term promotion flows", async () => {
|
|
const workspaceDir = await createTempWorkspace("memory-core-events-");
|
|
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, "memory", "2026-04-05.md"),
|
|
"# Daily\n\nalpha\nbeta\ngamma\n",
|
|
"utf8",
|
|
);
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "alpha memory",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-05.md",
|
|
startLine: 3,
|
|
endLine: 4,
|
|
score: 0.92,
|
|
snippet: "alpha beta",
|
|
source: "memory",
|
|
},
|
|
],
|
|
nowMs: Date.UTC(2026, 3, 5, 12, 0, 0),
|
|
});
|
|
|
|
const candidates = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.UTC(2026, 3, 5, 12, 5, 0),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.UTC(2026, 3, 5, 12, 10, 0),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
|
|
const events = await readMemoryHostEvents({ workspaceDir });
|
|
|
|
expect(events.map((event) => event.type)).toEqual([
|
|
"memory.recall.recorded",
|
|
"memory.promotion.applied",
|
|
]);
|
|
const recallEvent = events[0];
|
|
if (recallEvent?.type !== "memory.recall.recorded") {
|
|
throw new Error("expected recall event");
|
|
}
|
|
expect(recallEvent.resultCount).toBe(1);
|
|
expect(recallEvent.query).toBe("alpha memory");
|
|
const promotionEvent = events[1];
|
|
if (promotionEvent?.type !== "memory.promotion.applied") {
|
|
throw new Error("expected promotion event");
|
|
}
|
|
expect(promotionEvent.applied).toBe(1);
|
|
});
|
|
|
|
it("records skipped recall events for durable memory hits excluded from short-term promotion", async () => {
|
|
const workspaceDir = await createTempWorkspace("memory-core-skipped-recall-events-");
|
|
await fs.mkdir(path.join(workspaceDir, "memory", "decisoes"), { recursive: true });
|
|
await fs.mkdir(path.join(workspaceDir, "memory", "idiomas"), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, "MEMORY.md"),
|
|
"# Memory\n\nAlpha durable note.\n",
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, "memory", "decisoes", "2026-06.md"),
|
|
"# Decisoes\n\nAlpha monthly decision.\n",
|
|
"utf8",
|
|
);
|
|
await fs.writeFile(
|
|
path.join(workspaceDir, "memory", "idiomas", "PLANO.md"),
|
|
"# Plano\n\nAlpha language plan.\n",
|
|
"utf8",
|
|
);
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "alpha durable memory",
|
|
results: [
|
|
{
|
|
path: "MEMORY.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.91,
|
|
snippet: "Alpha durable note.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/decisoes/2026-06.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.88,
|
|
snippet: "Alpha monthly decision.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/idiomas/PLANO.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.83,
|
|
snippet: "Alpha language plan.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
nowMs: Date.UTC(2026, 5, 13, 9, 0, 0),
|
|
});
|
|
|
|
const candidates = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.UTC(2026, 5, 13, 9, 5, 0),
|
|
});
|
|
const events = await readMemoryHostEventRecords({ workspaceDir });
|
|
|
|
expect(candidates).toEqual([]);
|
|
expect(events.map((event) => event.type)).toEqual(["memory.recall.skipped"]);
|
|
const skippedEvent = events[0];
|
|
if (skippedEvent?.type !== "memory.recall.skipped") {
|
|
throw new Error("expected skipped recall event");
|
|
}
|
|
expect(skippedEvent.query).toBe("alpha durable memory");
|
|
expect(skippedEvent.reason).toBe("non-short-term-memory-path");
|
|
expect(skippedEvent.eligibleResultCount).toBe(0);
|
|
expect(skippedEvent.skippedResultCount).toBe(3);
|
|
expect(skippedEvent.results.map((result) => result.path)).toEqual([
|
|
"MEMORY.md",
|
|
"memory/decisoes/2026-06.md",
|
|
"memory/idiomas/PLANO.md",
|
|
]);
|
|
expect(
|
|
skippedEvent.results.every((result) => result.reason === "non-short-term-memory-path"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("records dreaming completion events when phase artifacts are written", async () => {
|
|
const workspaceDir = await createTempWorkspace("memory-core-dream-events-");
|
|
|
|
const written = await writeDailyDreamingPhaseBlock({
|
|
workspaceDir,
|
|
phase: "light",
|
|
bodyLines: ["- staged note", "- second note"],
|
|
nowMs: Date.UTC(2026, 3, 5, 13, 0, 0),
|
|
storage: { mode: "both", separateReports: true },
|
|
});
|
|
|
|
const events = await readMemoryHostEvents({ workspaceDir });
|
|
|
|
expect(written.inlinePath).toBe(path.join(workspaceDir, "memory", "2026-04-05.md"));
|
|
expect(written.reportPath).toBe(
|
|
path.join(workspaceDir, "memory", "dreaming", "light", "2026-04-05.md"),
|
|
);
|
|
await expect(fs.readFile(written.inlinePath ?? "", "utf8")).resolves.toContain("- staged note");
|
|
await expect(fs.readFile(written.reportPath ?? "", "utf8")).resolves.toContain("- second note");
|
|
expect(events).toHaveLength(1);
|
|
const dreamEvent = events[0];
|
|
if (dreamEvent?.type !== "memory.dream.completed") {
|
|
throw new Error("expected dream completion event");
|
|
}
|
|
expect(dreamEvent.phase).toBe("light");
|
|
expect(dreamEvent.outcome).toBe("completed");
|
|
expect(dreamEvent.lineCount).toBe(2);
|
|
expect(dreamEvent.storageMode).toBe("both");
|
|
});
|
|
|
|
it("keeps legacy dreaming completion events without outcome readable", async () => {
|
|
const workspaceDir = await createTempWorkspace("memory-core-legacy-dream-events-");
|
|
const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir);
|
|
await fs.mkdir(path.dirname(eventLogPath), { recursive: true });
|
|
await fs.writeFile(
|
|
eventLogPath,
|
|
`${JSON.stringify({
|
|
type: "memory.dream.completed",
|
|
timestamp: "2026-04-05T13:00:00.000Z",
|
|
phase: "deep",
|
|
inlinePath: path.join(workspaceDir, "DREAMS.md"),
|
|
lineCount: 2,
|
|
storageMode: "inline",
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
const events = await readMemoryHostEvents({ workspaceDir });
|
|
|
|
expect(events).toHaveLength(1);
|
|
const dreamEvent = events[0];
|
|
if (dreamEvent?.type !== "memory.dream.completed") {
|
|
throw new Error("expected dream completion event");
|
|
}
|
|
expect(dreamEvent.outcome).toBeUndefined();
|
|
expect(dreamEvent.phase).toBe("deep");
|
|
});
|
|
});
|