mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 23:51:48 +00:00
Merged via squash.
Prepared head SHA: 5dfe246ef9
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
1390 lines
42 KiB
TypeScript
1390 lines
42 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
|
|
appendMemoryHostEvent: vi.fn(async () => {}),
|
|
}));
|
|
|
|
import {
|
|
applyShortTermPromotions,
|
|
auditShortTermPromotionArtifacts,
|
|
isShortTermMemoryPath,
|
|
recordGroundedShortTermCandidates,
|
|
rankShortTermPromotionCandidates,
|
|
recordDreamingPhaseSignals,
|
|
recordShortTermRecalls,
|
|
removeGroundedShortTermCandidates,
|
|
repairShortTermPromotionArtifacts,
|
|
resolveShortTermRecallLockPath,
|
|
resolveShortTermPhaseSignalStorePath,
|
|
resolveShortTermRecallStorePath,
|
|
__testing,
|
|
} from "./short-term-promotion.js";
|
|
|
|
describe("short-term promotion", () => {
|
|
let fixtureRoot = "";
|
|
let caseId = 0;
|
|
|
|
beforeAll(async () => {
|
|
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-promote-"));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (!fixtureRoot) {
|
|
return;
|
|
}
|
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
async function withTempWorkspace(run: (workspaceDir: string) => Promise<void>) {
|
|
const workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
|
|
await fs.mkdir(path.join(workspaceDir, "memory", ".dreams"), { recursive: true });
|
|
await run(workspaceDir);
|
|
}
|
|
|
|
async function writeDailyMemoryNote(
|
|
workspaceDir: string,
|
|
date: string,
|
|
lines: string[],
|
|
): Promise<string> {
|
|
const notePath = path.join(workspaceDir, "memory", `${date}.md`);
|
|
await fs.writeFile(notePath, `${lines.join("\n")}\n`, "utf-8");
|
|
return notePath;
|
|
}
|
|
|
|
it("detects short-term daily memory paths", () => {
|
|
expect(isShortTermMemoryPath("memory/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/.dreams/session-corpus/2026-04-03.txt")).toBe(true);
|
|
expect(isShortTermMemoryPath("notes/2026-04-03.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("MEMORY.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("memory/network.md")).toBe(false);
|
|
});
|
|
|
|
it("records recalls and ranks candidates with weighted scores", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 3,
|
|
endLine: 5,
|
|
score: 0.9,
|
|
snippet: "Configured VLAN 10 on Omada router",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "MEMORY.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.99,
|
|
snippet: "Long-term note",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "iot vlan",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 3,
|
|
endLine: 5,
|
|
score: 0.8,
|
|
snippet: "Configured VLAN 10 on Omada router",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.path).toBe("memory/2026-04-02.md");
|
|
expect(ranked[0]?.recallCount).toBe(2);
|
|
expect(ranked[0]?.uniqueQueries).toBe(2);
|
|
expect(ranked[0]?.score).toBeGreaterThan(0);
|
|
expect(ranked[0]?.conceptTags).toContain("router");
|
|
expect(ranked[0]?.components.conceptual).toBeGreaterThan(0);
|
|
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
const raw = await fs.readFile(storePath, "utf-8");
|
|
expect(raw).toContain("memory/2026-04-02.md");
|
|
expect(raw).not.toContain("Long-term note");
|
|
});
|
|
});
|
|
|
|
it("serializes concurrent recall writes so counts are not lost", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await Promise.all(
|
|
Array.from({ length: 8 }, (_, index) =>
|
|
recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: `backup-${index % 4}`,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
);
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.recallCount).toBe(8);
|
|
expect(ranked[0]?.uniqueQueries).toBe(4);
|
|
});
|
|
});
|
|
|
|
it("uses default thresholds for promotion", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.96,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({ workspaceDir });
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("lets grounded durable evidence satisfy default deep thresholds", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
|
'Always use "Happy Together" calendar for flights and reservations.',
|
|
]);
|
|
|
|
await recordGroundedShortTermCandidates({
|
|
workspaceDir,
|
|
query: "__dreaming_grounded_backfill__",
|
|
items: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: 'Always use "Happy Together" calendar for flights and reservations.',
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: 'Always use "Happy Together" calendar for flights and reservations.',
|
|
score: 0.82,
|
|
query: "__dreaming_grounded_backfill__:candidate",
|
|
signalCount: 1,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
],
|
|
dedupeByQueryPerDay: true,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.groundedCount).toBe(3);
|
|
expect(ranked[0]?.uniqueQueries).toBe(2);
|
|
expect(ranked[0]?.avgScore).toBeGreaterThan(0.85);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memory = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memory).toContain('Always use "Happy Together" calendar');
|
|
});
|
|
});
|
|
|
|
it("removes grounded-only staged entries without deleting mixed live entries", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
|
"Grounded only rule.",
|
|
"Live recall-backed rule.",
|
|
]);
|
|
|
|
await recordGroundedShortTermCandidates({
|
|
workspaceDir,
|
|
query: "__dreaming_grounded_backfill__",
|
|
items: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: "Grounded only rule.",
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
snippet: "Live recall-backed rule.",
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
],
|
|
dedupeByQueryPerDay: true,
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "live recall",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
score: 0.87,
|
|
snippet: "Live recall-backed rule.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await removeGroundedShortTermCandidates({ workspaceDir });
|
|
expect(result.removed).toBe(1);
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.snippet).toContain("Live recall-backed rule");
|
|
expect(ranked[0]?.groundedCount).toBe(2);
|
|
expect(ranked[0]?.recallCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("rewards spaced recalls as consolidation instead of only raw count", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.9,
|
|
snippet: "Configured router VLAN 10 and IoT segment.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "iot segment",
|
|
nowMs: Date.parse("2026-04-04T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.88,
|
|
snippet: "Configured router VLAN 10 and IoT segment.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.recallDays).toEqual(["2026-04-01", "2026-04-04"]);
|
|
expect(ranked[0]?.components.consolidation).toBeGreaterThan(0.4);
|
|
});
|
|
});
|
|
|
|
it("lets recency half-life tune the temporal score", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier retention",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.92,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const slowerDecay = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
recencyHalfLifeDays: 14,
|
|
});
|
|
const fasterDecay = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
recencyHalfLifeDays: 7,
|
|
});
|
|
|
|
expect(slowerDecay).toHaveLength(1);
|
|
expect(fasterDecay).toHaveLength(1);
|
|
expect(slowerDecay[0]?.components.recency).toBeCloseTo(0.5, 3);
|
|
expect(fasterDecay[0]?.components.recency).toBeCloseTo(0.25, 3);
|
|
expect(slowerDecay[0].score).toBeGreaterThan(fasterDecay[0].score);
|
|
});
|
|
});
|
|
|
|
it("boosts deep ranking when light/rem phase signals reinforce a candidate", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const nowMs = Date.parse("2026-04-05T10:00:00.000Z");
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router setup",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Router VLAN baseline noted.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Backup policy for router snapshots.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router backup",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Router VLAN baseline noted.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Backup policy for router snapshots.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const baseline = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
expect(baseline).toHaveLength(2);
|
|
expect(baseline[0]?.path).toBe("memory/2026-04-01.md");
|
|
|
|
const boostedKey = baseline.find((entry) => entry.path === "memory/2026-04-02.md")?.key;
|
|
expect(boostedKey).toBeTruthy();
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "light",
|
|
keys: [boostedKey!],
|
|
nowMs,
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [boostedKey!],
|
|
nowMs,
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
expect(ranked[0]?.path).toBe("memory/2026-04-02.md");
|
|
expect(ranked[0].score).toBeGreaterThan(ranked[1].score);
|
|
|
|
const phaseStorePath = resolveShortTermPhaseSignalStorePath(workspaceDir);
|
|
const phaseStore = JSON.parse(await fs.readFile(phaseStorePath, "utf-8")) as {
|
|
entries: Record<string, { lightHits: number; remHits: number }>;
|
|
};
|
|
expect(phaseStore.entries[boostedKey!]).toMatchObject({
|
|
lightHits: 1,
|
|
remHits: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("weights fresh phase signals more than stale ones", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier cadence",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "backup lifecycle",
|
|
nowMs: Date.parse("2026-04-01T12:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const rankedBaseline = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
const key = rankedBaseline[0]?.key;
|
|
expect(key).toBeTruthy();
|
|
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [key],
|
|
nowMs: Date.parse("2026-02-01T10:00:00.000Z"),
|
|
});
|
|
const staleSignalRank = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [key],
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
const freshSignalRank = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(staleSignalRank).toHaveLength(1);
|
|
expect(freshSignalRank).toHaveLength(1);
|
|
expect(freshSignalRank[0].score).toBeGreaterThan(staleSignalRank[0].score);
|
|
});
|
|
});
|
|
|
|
it("reconciles existing promotion markers instead of appending duplicates", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"line 1",
|
|
"line 2",
|
|
"The gateway should stay loopback-only on port 18789.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway loopback",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.95,
|
|
snippet: "The gateway should stay loopback-only on port 18789.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const firstApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(firstApply.applied).toBe(1);
|
|
expect(firstApply.appended).toBe(1);
|
|
expect(firstApply.reconciledExisting).toBe(0);
|
|
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as {
|
|
entries: Record<string, { promotedAt?: string }>;
|
|
};
|
|
for (const entry of Object.values(rawStore.entries)) {
|
|
delete entry.promotedAt;
|
|
}
|
|
await fs.writeFile(storePath, `${JSON.stringify(rawStore, null, 2)}\n`, "utf-8");
|
|
|
|
const secondApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(secondApply.applied).toBe(1);
|
|
expect(secondApply.appended).toBe(0);
|
|
expect(secondApply.reconciledExisting).toBe(1);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText.match(/openclaw-memory-promotion:/g)?.length).toBe(1);
|
|
expect(
|
|
memoryText.match(/The gateway should stay loopback-only on port 18789\./g)?.length,
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("filters out candidates older than maxAgeDays during ranking", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "old note",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.92,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
maxAgeDays: 7,
|
|
});
|
|
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("treats negative threshold overrides as invalid and keeps defaults", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.96,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: -1,
|
|
minRecallCount: -1,
|
|
minUniqueQueries: -1,
|
|
});
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("enforces default thresholds during apply even when candidates are passed directly", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-03.md:1:2",
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet: "Move backups to S3 Glacier.",
|
|
recallCount: 1,
|
|
avgScore: 0.95,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 1,
|
|
firstRecalledAt: new Date().toISOString(),
|
|
lastRecalledAt: new Date().toISOString(),
|
|
ageDays: 0,
|
|
score: 0.95,
|
|
recallDays: [new Date().toISOString().slice(0, 10)],
|
|
conceptTags: ["glacier", "backups"],
|
|
components: {
|
|
frequency: 0.2,
|
|
relevance: 0.95,
|
|
diversity: 0.2,
|
|
recency: 1,
|
|
consolidation: 0.2,
|
|
conceptual: 0.4,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
maxAgeDays: 7,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-01.md:1:1",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet: "Expired short-term note.",
|
|
recallCount: 3,
|
|
avgScore: 0.95,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-02T00:00:00.000Z",
|
|
ageDays: 10,
|
|
score: 0.95,
|
|
recallDays: ["2026-04-01", "2026-04-02"],
|
|
conceptTags: ["expired"],
|
|
components: {
|
|
frequency: 1,
|
|
relevance: 1,
|
|
diversity: 1,
|
|
recency: 1,
|
|
consolidation: 1,
|
|
conceptual: 1,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
await expect(
|
|
fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"),
|
|
).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("applies promotion candidates to MEMORY.md and marks them promoted", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"alpha",
|
|
"beta",
|
|
"gamma",
|
|
"delta",
|
|
"epsilon",
|
|
"zeta",
|
|
"eta",
|
|
"theta",
|
|
"iota",
|
|
"Gateway binds loopback and port 18789",
|
|
"Keep gateway on localhost only",
|
|
"Document healthcheck endpoint",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway host",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 10,
|
|
endLine: 12,
|
|
score: 0.92,
|
|
snippet: "Gateway binds loopback and port 18789",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(applied.applied).toBe(1);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("Promoted From Short-Term Memory");
|
|
expect(memoryText).toContain("memory/2026-04-01.md:10-10");
|
|
|
|
const rankedAfter = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(rankedAfter).toHaveLength(0);
|
|
|
|
const rankedIncludingPromoted = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
includePromoted: true,
|
|
});
|
|
expect(rankedIncludingPromoted).toHaveLength(1);
|
|
expect(rankedIncludingPromoted[0]?.promotedAt).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it("does not re-append candidates that were promoted in a prior run", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"alpha",
|
|
"beta",
|
|
"gamma",
|
|
"delta",
|
|
"epsilon",
|
|
"zeta",
|
|
"eta",
|
|
"theta",
|
|
"iota",
|
|
"Gateway binds loopback and port 18789",
|
|
"Keep gateway on localhost only",
|
|
"Document healthcheck endpoint",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway host",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 10,
|
|
endLine: 12,
|
|
score: 0.92,
|
|
snippet: "Gateway binds loopback and port 18789",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const first = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(first.applied).toBe(1);
|
|
|
|
const second = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(second.applied).toBe(0);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
const sectionCount = memoryText.match(/Promoted From Short-Term Memory/g)?.length ?? 0;
|
|
expect(sectionCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("rehydrates moved snippets from the live daily note before promotion", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"intro",
|
|
"summary",
|
|
"Moved backups to S3 Glacier.",
|
|
"Keep cold storage retention at 365 days.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet: "Moved backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(3);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(3);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("memory/2026-04-01.md:3-3");
|
|
});
|
|
});
|
|
|
|
it("prefers the nearest matching snippet when the same text appears multiple times", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"header",
|
|
"Repeat backup note.",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"Repeat backup note.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "backup repeat",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 8,
|
|
endLine: 9,
|
|
score: 0.9,
|
|
snippet: "Repeat backup note.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(9);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(10);
|
|
});
|
|
});
|
|
|
|
it("rehydrates legacy basename-only short-term paths from the memory directory", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Legacy basename path note."]);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: [
|
|
{
|
|
key: "memory:2026-04-01.md:1:1",
|
|
path: "2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet: "Legacy basename path note.",
|
|
recallCount: 2,
|
|
avgScore: 0.9,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-02T00:00:00.000Z",
|
|
ageDays: 0,
|
|
score: 0.9,
|
|
recallDays: ["2026-04-01", "2026-04-02"],
|
|
conceptTags: ["legacy", "note"],
|
|
components: {
|
|
frequency: 0.3,
|
|
relevance: 0.9,
|
|
diversity: 0.4,
|
|
recency: 1,
|
|
consolidation: 0.5,
|
|
conceptual: 0.3,
|
|
},
|
|
},
|
|
],
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("source=2026-04-01.md:1-1");
|
|
});
|
|
});
|
|
|
|
it("skips promotion when the live daily note no longer contains the snippet", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Different note content now."]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet: "Moved backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
await expect(fs.access(path.join(workspaceDir, "MEMORY.md"))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("uses dreaming timezone for recall-day bucketing and promotion headers", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"Cross-midnight router maintenance window.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router window",
|
|
nowMs: Date.parse("2026-04-01T23:30:00.000Z"),
|
|
timezone: "America/Los_Angeles",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Cross-midnight router maintenance window.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked[0]?.recallDays).toEqual(["2026-04-01"]);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-02T06:30:00.000Z"),
|
|
timezone: "America/Los_Angeles",
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("Promoted From Short-Term Memory (2026-04-01)");
|
|
});
|
|
});
|
|
|
|
it("audits and repairs invalid store metadata plus stale locks", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
await fs.writeFile(
|
|
storePath,
|
|
JSON.stringify(
|
|
{
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
good: {
|
|
key: "good",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet: "Gateway host uses qmd vector search for router notes.",
|
|
recallCount: 2,
|
|
totalScore: 1.8,
|
|
maxScore: 0.95,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
queryHashes: ["a", "b"],
|
|
},
|
|
bad: {
|
|
path: "",
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf-8",
|
|
);
|
|
|
|
const lockPath = path.join(workspaceDir, "memory", ".dreams", "short-term-promotion.lock");
|
|
await fs.writeFile(lockPath, "999999:0\n", "utf-8");
|
|
const staleMtime = new Date(Date.now() - 120_000);
|
|
await fs.utimes(lockPath, staleMtime, staleMtime);
|
|
|
|
const auditBefore = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(auditBefore.invalidEntryCount).toBe(1);
|
|
expect(auditBefore.issues.map((issue) => issue.code)).toEqual(
|
|
expect.arrayContaining(["recall-store-invalid", "recall-lock-stale"]),
|
|
);
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(repair.removedStaleLock).toBe(true);
|
|
|
|
const auditAfter = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(auditAfter.invalidEntryCount).toBe(0);
|
|
expect(auditAfter.issues.map((issue) => issue.code)).not.toContain("recall-lock-stale");
|
|
|
|
const repairedRaw = JSON.parse(await fs.readFile(storePath, "utf-8")) as {
|
|
entries: Record<string, { conceptTags?: string[]; recallDays?: string[] }>;
|
|
};
|
|
expect(repairedRaw.entries.good?.conceptTags).toContain("router");
|
|
expect(repairedRaw.entries.good?.recallDays).toEqual(["2026-04-04"]);
|
|
});
|
|
});
|
|
|
|
it("repairs empty recall-store files without throwing", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
await fs.writeFile(storePath, " \n", "utf-8");
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(JSON.parse(await fs.readFile(storePath, "utf-8"))).toMatchObject({
|
|
version: 1,
|
|
entries: {},
|
|
});
|
|
});
|
|
});
|
|
|
|
it("does not rewrite an already normalized healthy recall store", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
const snippet = "Gateway host uses qmd vector search for router notes.";
|
|
const raw = `${JSON.stringify(
|
|
{
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
good: {
|
|
key: "good",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet,
|
|
recallCount: 2,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: 1.8,
|
|
maxScore: 0.95,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
queryHashes: ["a", "b"],
|
|
recallDays: ["2026-04-04"],
|
|
conceptTags: __testing.deriveConceptTags({
|
|
path: "memory/2026-04-01.md",
|
|
snippet,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`;
|
|
await fs.writeFile(storePath, raw, "utf-8");
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(false);
|
|
expect(repair.rewroteStore).toBe(false);
|
|
const nextRaw = await fs.readFile(storePath, "utf-8");
|
|
expect(nextRaw).toBe(raw);
|
|
});
|
|
});
|
|
|
|
it("waits for an active short-term lock before repairing", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const storePath = resolveShortTermRecallStorePath(workspaceDir);
|
|
const lockPath = resolveShortTermRecallLockPath(workspaceDir);
|
|
await fs.writeFile(
|
|
storePath,
|
|
JSON.stringify(
|
|
{
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
bad: {
|
|
path: "",
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf-8",
|
|
);
|
|
await fs.writeFile(lockPath, `${process.pid}:${Date.now()}\n`, "utf-8");
|
|
|
|
let settled = false;
|
|
const repairPromise = repairShortTermPromotionArtifacts({ workspaceDir }).then((result) => {
|
|
settled = true;
|
|
return result;
|
|
});
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 41));
|
|
expect(settled).toBe(false);
|
|
|
|
await fs.unlink(lockPath);
|
|
const repair = await repairPromise;
|
|
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(repair.removedInvalidEntries).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("downgrades lock inspection failures into audit issues", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const lockPath = path.join(workspaceDir, "memory", ".dreams", "short-term-promotion.lock");
|
|
const stat = vi.spyOn(fs, "stat").mockImplementation(async (target) => {
|
|
if (String(target) === lockPath) {
|
|
const error = Object.assign(new Error("no access"), { code: "EACCES" });
|
|
throw error;
|
|
}
|
|
return await vi
|
|
.importActual<typeof import("node:fs/promises")>("node:fs/promises")
|
|
.then((actual) => actual.stat(target));
|
|
});
|
|
try {
|
|
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(audit.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: "recall-lock-unreadable",
|
|
fixable: false,
|
|
}),
|
|
]),
|
|
);
|
|
} finally {
|
|
stat.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reports concept tag script coverage for multilingual recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "routeur glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.93,
|
|
snippet: "Configuration du routeur et sauvegarde Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router cjk",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-04.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.95,
|
|
snippet: "障害対応ルーター設定とバックアップ確認。",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(audit.conceptTaggedEntryCount).toBe(2);
|
|
expect(audit.conceptTagScripts).toEqual({
|
|
latinEntryCount: 1,
|
|
cjkEntryCount: 1,
|
|
mixedEntryCount: 0,
|
|
otherEntryCount: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("extracts stable concept tags from snippets and paths", () => {
|
|
expect(
|
|
__testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "Move backups to S3 Glacier and sync QMD router notes.",
|
|
}),
|
|
).toEqual(expect.arrayContaining(["glacier", "router", "backups"]));
|
|
});
|
|
|
|
it("extracts multilingual concept tags across latin and cjk snippets", () => {
|
|
expect(
|
|
__testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "Configuración du routeur et sauvegarde Glacier.",
|
|
}),
|
|
).toEqual(expect.arrayContaining(["configuración", "routeur", "sauvegarde", "glacier"]));
|
|
expect(
|
|
__testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "障害対応ルーター設定とバックアップ確認。路由器备份与网关同步。",
|
|
}),
|
|
).toEqual(expect.arrayContaining(["障害対応", "ルーター", "バックアップ", "路由器", "备份"]));
|
|
});
|
|
});
|