import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildTriggerRecallContext, isPromotedTrustedMemoryEntry, MAX_TRIGGER_CONTEXT_CHARS, prewarmTriggerRecall, scoreTriggerMatch, resolveTriggerRecall, selectStrongTriggerMatches, STRONG_TRIGGER_MATCH_SCORE, } from "./trigger-recall.js"; const hoisted = vi.hoisted(() => ({ getManager: vi.fn(), search: vi.fn(), listTriggerCandidates: vi.fn(), })); vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({ getActiveMemorySearchManager: (...args: unknown[]) => hoisted.getManager(...args), })); function result(overrides: Partial = {}): MemorySearchResult { return { path: "MEMORY.md", startLine: 1, endLine: 2, score: 0.8, snippet: "User prefers aisle seats and extra connection time.", source: "memory", triggers: "when booking a flight; seat preferences", ...overrides, }; } describe("active-memory trigger recall", () => { beforeEach(() => { hoisted.getManager.mockReset().mockResolvedValue({ manager: { search: hoisted.search, listTriggerCandidates: hoisted.listTriggerCandidates }, }); hoisted.search.mockReset(); hoisted.listTriggerCandidates.mockReset(); }); it("matches trigger phrases deterministically", () => { expect(scoreTriggerMatch("Can you help when booking a flight?", result())).toBeGreaterThan(0.8); expect(scoreTriggerMatch("Explain SQLite indexes", result())).toBeLessThan(0.5); expect( scoreTriggerMatch("This party starts at eight", result({ score: 0.2, triggers: "art" })), ).toBeLessThan(0.65); // Single-word concept triggers (the promotion writer's output) cap at // 0.85 * 0.8 = 0.68 with zero relevance; the 0.65 threshold must admit them. expect( scoreTriggerMatch("Project status", result({ score: 0, triggers: "project" })), ).toBeCloseTo(0.68); expect( scoreTriggerMatch("Project status", result({ score: 0, triggers: "project" })), ).toBeGreaterThanOrEqual(STRONG_TRIGGER_MATCH_SCORE); }); it("limits automatic injection to curated or trusted-origin entries", () => { expect(isPromotedTrustedMemoryEntry(result())).toBe(true); expect(isPromotedTrustedMemoryEntry(result({ path: "USER.md" }))).toBe(true); expect(isPromotedTrustedMemoryEntry(result({ path: "memory/2026-07-27.md" }))).toBe(false); expect(isPromotedTrustedMemoryEntry(result({ source: "sessions" }))).toBe(false); expect( isPromotedTrustedMemoryEntry(result({ path: "memory/promoted.md", originClass: "owner" })), ).toBe(true); const matches = selectStrongTriggerMatches("when booking a flight", [ result(), result({ path: "USER.md", startLine: 3 }), result({ path: "memory/2026-07-27.md", startLine: 4 }), result({ source: "sessions", path: "session.jsonl", startLine: 5 }), ]); expect(matches.map((entry) => entry.path)).toEqual(["MEMORY.md", "USER.md"]); const provenanceMatches = selectStrongTriggerMatches("when booking a flight", [ result({ path: "memory/untrusted.md", originClass: "untrusted", score: 1 }), result({ path: "memory/owner.md", originClass: "owner", score: 1 }), ]); expect(provenanceMatches.map((entry) => entry.path)).toEqual(["memory/owner.md"]); }); it("gates tagged entries to the active project while leaving global entries unchanged", () => { const activeKey = "github.com/OpenClaw/OpenClaw"; const sameProject = result({ projectKey: activeKey, startLine: 1 }); const foreignProject = result({ projectKey: "github.com/example/other", startLine: 2 }); const global = result({ startLine: 3 }); expect( selectStrongTriggerMatches( "when booking a flight", [sameProject, foreignProject, global], [activeKey], ).map((entry) => entry.startLine), ).toEqual([1, 3]); expect( selectStrongTriggerMatches( "when booking a flight", [sameProject, foreignProject, global], [], ), ).toHaveLength(1); }); it("selects and injects only the matching curated entry within the active project", () => { const alpha = result({ startLine: 1, endLine: 1, snippet: "Alpha-only deployment guidance.", triggers: "alpha deployment", projectKey: "alpha-key", }); const beta = result({ startLine: 2, endLine: 2, snippet: "Beta-only deployment guidance.", triggers: "beta deployment", projectKey: "beta-key", }); const global = result({ startLine: 3, endLine: 3, snippet: "Global deployment guidance.", triggers: "global deployment", }); const entries = [alpha, beta, global]; const alphaMatches = selectStrongTriggerMatches("Review the alpha deployment", entries, [ "alpha-key", ]); expect(alphaMatches.map((entry) => entry.snippet)).toEqual(["Alpha-only deployment guidance."]); expect( selectStrongTriggerMatches("Review the global deployment", entries, ["alpha-key"]).map( (entry) => entry.snippet, ), ).toEqual(["Global deployment guidance."]); expect( selectStrongTriggerMatches("Review the beta deployment", entries, ["alpha-key"]), ).toEqual([]); const context = buildTriggerRecallContext(alphaMatches); expect(context).toContain("Alpha-only deployment guidance."); expect(context).not.toContain("Beta-only deployment guidance."); expect(context).not.toContain("Global deployment guidance."); }); it("excludes annotation carriers from injected trigger context", () => { const matches = selectStrongTriggerMatches( "Review the alpha deployment", [ result({ snippet: "Alpha-only deployment guidance. ", triggers: "alpha deployment", projectKey: "alpha-key", }), ], ["alpha-key"], ); const context = buildTriggerRecallContext(matches); expect(context).toContain("Alpha-only deployment guidance."); expect(context).not.toContain("