mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
test: consolidate fixture setup (#130382)
* test(wizard): consolidate finalize fixtures * test(memory-lancedb): deduplicate table fixtures * test(openshell): consolidate manager fixtures * test(memory): consolidate promotion evidence fixtures * test(openshell): preserve remote seed fixture ownership
This commit is contained in:
committed by
GitHub
parent
f290d6bc81
commit
b3db4758a3
@@ -39,6 +39,7 @@ import {
|
||||
readLightStagedKeys,
|
||||
removeGroundedShortTermCandidates,
|
||||
repairShortTermPromotionArtifacts,
|
||||
type ShortTermRecallEntry,
|
||||
} from "./short-term-promotion.js";
|
||||
import {
|
||||
configureMemoryCoreDreamingStateForTests,
|
||||
@@ -60,6 +61,9 @@ type ApplyAllOptions = Omit<
|
||||
"workspaceDir" | "candidates" | "minScore" | "minRecallCount" | "minUniqueQueries"
|
||||
>;
|
||||
type PromotionCandidate = Awaited<ReturnType<typeof rankShortTermPromotionCandidates>>[number];
|
||||
type GroundedCandidateFixture = Parameters<
|
||||
typeof recordGroundedShortTermCandidates
|
||||
>[0]["items"][number];
|
||||
type PromotionCandidateFixture = Pick<
|
||||
PromotionCandidate,
|
||||
"key" | "path" | "startLine" | "endLine" | "source" | "snippet"
|
||||
@@ -134,6 +138,42 @@ function promotionCandidateFixture(params: PromotionCandidateFixture): Promotion
|
||||
};
|
||||
}
|
||||
|
||||
function recallStoreEntryFixture(
|
||||
params: Pick<ShortTermRecallEntry, "key" | "path"> & Partial<ShortTermRecallEntry>,
|
||||
): ShortTermRecallEntry {
|
||||
return {
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory",
|
||||
snippet: `${params.key} recall`,
|
||||
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: [],
|
||||
...params,
|
||||
};
|
||||
}
|
||||
|
||||
function groundedCandidateFixture(
|
||||
params: Pick<GroundedCandidateFixture, "path" | "snippet" | "query"> &
|
||||
Partial<GroundedCandidateFixture>,
|
||||
): GroundedCandidateFixture {
|
||||
return {
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.9,
|
||||
signalCount: 1,
|
||||
dayBucket: "2026-04-03",
|
||||
...params,
|
||||
};
|
||||
}
|
||||
|
||||
describe("short-term promotion", () => {
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
@@ -220,6 +260,50 @@ describe("short-term promotion", () => {
|
||||
return notePath;
|
||||
}
|
||||
|
||||
async function seedGatewayPromotionCandidate(workspaceDir: string) {
|
||||
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 recordMemoryRecalls(workspaceDir, "gateway host", [
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-01.md",
|
||||
10,
|
||||
12,
|
||||
0.92,
|
||||
"Gateway binds loopback and port 18789",
|
||||
),
|
||||
]);
|
||||
return await rankAllCandidates(workspaceDir);
|
||||
}
|
||||
|
||||
function recordRotateCredentialsRecall(workspaceDir: string): Promise<void> {
|
||||
return recordMemoryRecalls(
|
||||
workspaceDir,
|
||||
"rotate creds",
|
||||
[
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-29.md",
|
||||
3,
|
||||
3,
|
||||
0.96,
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
),
|
||||
],
|
||||
{ nowMs: Date.parse("2026-04-29T10:00:00.000Z") },
|
||||
);
|
||||
}
|
||||
|
||||
function requireCandidateKey(
|
||||
candidate: { key?: string } | null | undefined,
|
||||
label: string,
|
||||
@@ -240,25 +324,20 @@ describe("short-term promotion", () => {
|
||||
return candidate.promotedAt;
|
||||
}
|
||||
|
||||
async function readRecallStoreEntries(workspaceDir: string): Promise<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
claimHash?: unknown;
|
||||
firstRecalledAt?: unknown;
|
||||
lastRecalledAt?: unknown;
|
||||
dailyCount?: unknown;
|
||||
recallCount?: unknown;
|
||||
snippet?: unknown;
|
||||
totalScore?: unknown;
|
||||
}
|
||||
>
|
||||
> {
|
||||
async function readRecallStoreEntries(workspaceDir: string) {
|
||||
return await testing
|
||||
.readRecallStore(workspaceDir, new Date().toISOString())
|
||||
.then((store) => store.entries);
|
||||
}
|
||||
|
||||
async function clearPromotedAt(workspaceDir: string): Promise<void> {
|
||||
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
for (const entry of Object.values(store.entries)) {
|
||||
delete entry.promotedAt;
|
||||
}
|
||||
await testing.writeRawRecallStore(workspaceDir, store);
|
||||
}
|
||||
|
||||
function readEntrySnippet(entry: { snippet?: unknown }): string {
|
||||
return typeof entry.snippet === "string" ? entry.snippet : "";
|
||||
}
|
||||
@@ -412,16 +491,11 @@ describe("short-term promotion", () => {
|
||||
workspaceDir,
|
||||
query: "__dreaming_grounded_backfill__",
|
||||
items: [
|
||||
{
|
||||
groundedCandidateFixture({
|
||||
path: "memory/2026-04-03.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
snippet: longSnippet,
|
||||
score: 0.9,
|
||||
query: "__dreaming_grounded_backfill__:candidate",
|
||||
signalCount: 1,
|
||||
dayBucket: "2026-04-03",
|
||||
},
|
||||
}),
|
||||
],
|
||||
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
||||
});
|
||||
@@ -448,14 +522,13 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores dream report paths when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "dream recall", [
|
||||
{
|
||||
path: "memory/dreaming/deep/2026-04-03.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.9,
|
||||
snippet: "Auto-generated dream report should not seed promotions.",
|
||||
},
|
||||
memoryRecallResult(
|
||||
"memory/dreaming/deep/2026-04-03.md",
|
||||
1,
|
||||
1,
|
||||
0.9,
|
||||
"Auto-generated dream report should not seed promotions.",
|
||||
),
|
||||
]);
|
||||
|
||||
expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
|
||||
@@ -463,14 +536,13 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores prefixed dream report paths when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "prefixed dream recall", [
|
||||
{
|
||||
path: "../../vault/memory/dreaming/deep/2026-04-03.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.9,
|
||||
snippet: "External dream report should not seed promotions.",
|
||||
},
|
||||
memoryRecallResult(
|
||||
"../../vault/memory/dreaming/deep/2026-04-03.md",
|
||||
1,
|
||||
1,
|
||||
0.9,
|
||||
"External dream report should not seed promotions.",
|
||||
),
|
||||
]);
|
||||
|
||||
expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
|
||||
@@ -478,15 +550,13 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores contaminated dreaming snippets when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "action preference", [
|
||||
{
|
||||
path: "memory/2026-04-03.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.92,
|
||||
snippet:
|
||||
"Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
||||
},
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-03.md",
|
||||
1,
|
||||
1,
|
||||
0.92,
|
||||
"Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
||||
),
|
||||
]);
|
||||
|
||||
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
@@ -496,20 +566,19 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores bullet-prefixed dreaming snippets when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "action preference", [
|
||||
{
|
||||
path: "memory/2026-04-03.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 5,
|
||||
score: 0.92,
|
||||
snippet: [
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-03.md",
|
||||
1,
|
||||
5,
|
||||
0.92,
|
||||
[
|
||||
"- Candidate: Default to action.",
|
||||
" - confidence: 0.76",
|
||||
" - evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1",
|
||||
" - recalls: 3",
|
||||
" - status: staged",
|
||||
].join("\n"),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
@@ -519,32 +588,27 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores raw session and transcript snippets when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "session recap", [
|
||||
{
|
||||
path: "memory/2026-06-18.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.92,
|
||||
snippet:
|
||||
"Session: 2026-06-18 10:37:05 EDT; Session Key: agent:cody:discord:channel:1502199757592989836; Session ID: 6d52b6a2-a2e1-4839-a69a-a532b9090a6d; Source: discord",
|
||||
},
|
||||
{
|
||||
path: "memory/2026-06-18.md",
|
||||
source: "memory",
|
||||
startLine: 2,
|
||||
endLine: 2,
|
||||
score: 0.91,
|
||||
snippet: "Conversation Summary: assistant: Traced all three. No changes made.",
|
||||
},
|
||||
{
|
||||
path: "memory/2026-06-18.md",
|
||||
source: "memory",
|
||||
startLine: 3,
|
||||
endLine: 3,
|
||||
score: 0.9,
|
||||
snippet:
|
||||
"user: Save important context from this session to the daily memory file. STRICT RULES: 1. The file MUST be named exactly memory/2026-06-18.md",
|
||||
},
|
||||
memoryRecallResult(
|
||||
"memory/2026-06-18.md",
|
||||
1,
|
||||
1,
|
||||
0.92,
|
||||
"Session: 2026-06-18 10:37:05 EDT; Session Key: agent:cody:discord:channel:1502199757592989836; Session ID: 6d52b6a2-a2e1-4839-a69a-a532b9090a6d; Source: discord",
|
||||
),
|
||||
memoryRecallResult(
|
||||
"memory/2026-06-18.md",
|
||||
2,
|
||||
2,
|
||||
0.91,
|
||||
"Conversation Summary: assistant: Traced all three. No changes made.",
|
||||
),
|
||||
memoryRecallResult(
|
||||
"memory/2026-06-18.md",
|
||||
3,
|
||||
3,
|
||||
0.9,
|
||||
"user: Save important context from this session to the daily memory file. STRICT RULES: 1. The file MUST be named exactly memory/2026-06-18.md",
|
||||
),
|
||||
]);
|
||||
|
||||
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
@@ -554,15 +618,13 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("ignores already-promoted score metadata snippets when recording short-term recalls", async (workspaceDir) => {
|
||||
await recordMemoryRecalls(workspaceDir, "promotion metadata", [
|
||||
{
|
||||
path: "memory/2026-06-18.md",
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.94,
|
||||
snippet:
|
||||
"2026-06-13 09:20 America/New_York - Polycore PR #112 re-review... [score=0.837 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
||||
},
|
||||
memoryRecallResult(
|
||||
"memory/2026-06-18.md",
|
||||
1,
|
||||
1,
|
||||
0.94,
|
||||
"2026-06-13 09:20 America/New_York - Polycore PR #112 re-review... [score=0.837 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
||||
),
|
||||
]);
|
||||
|
||||
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
@@ -855,36 +917,24 @@ describe("short-term promotion", () => {
|
||||
workspaceDir,
|
||||
query: "__dreaming_grounded_backfill__",
|
||||
items: [
|
||||
{
|
||||
groundedCandidateFixture({
|
||||
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: 1,
|
||||
dayBucket: "2026-04-03",
|
||||
},
|
||||
{
|
||||
}),
|
||||
groundedCandidateFixture({
|
||||
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",
|
||||
},
|
||||
{
|
||||
}),
|
||||
groundedCandidateFixture({
|
||||
path: "memory/2026-04-03.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
snippet: 'Always use "Happy Together" calendar for flights and reservations.',
|
||||
score: 0.86,
|
||||
query: "__dreaming_grounded_backfill__:durable-fact",
|
||||
signalCount: 1,
|
||||
dayBucket: "2026-04-03",
|
||||
},
|
||||
}),
|
||||
],
|
||||
dedupeByQueryPerDay: true,
|
||||
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
||||
@@ -921,17 +971,14 @@ describe("short-term promotion", () => {
|
||||
workspaceDir,
|
||||
query: "__dreaming_grounded_backfill__",
|
||||
items: [
|
||||
{
|
||||
groundedCandidateFixture({
|
||||
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",
|
||||
},
|
||||
{
|
||||
}),
|
||||
groundedCandidateFixture({
|
||||
path: "memory/2026-04-03.md",
|
||||
startLine: 2,
|
||||
endLine: 2,
|
||||
@@ -939,8 +986,7 @@ describe("short-term promotion", () => {
|
||||
score: 0.92,
|
||||
query: "__dreaming_grounded_backfill__:lasting-update",
|
||||
signalCount: 2,
|
||||
dayBucket: "2026-04-03",
|
||||
},
|
||||
}),
|
||||
],
|
||||
dedupeByQueryPerDay: true,
|
||||
});
|
||||
@@ -1250,11 +1296,7 @@ describe("short-term promotion", () => {
|
||||
expect(firstApply.appended).toBe(1);
|
||||
expect(firstApply.reconciledExisting).toBe(0);
|
||||
|
||||
const rawStore = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
for (const entry of Object.values(rawStore.entries)) {
|
||||
delete entry.promotedAt;
|
||||
}
|
||||
await testing.writeRawRecallStore(workspaceDir, rawStore);
|
||||
await clearPromotedAt(workspaceDir);
|
||||
|
||||
const secondApply = await applyAllCandidates(workspaceDir, ranked);
|
||||
expect(secondApply.applied).toBe(1);
|
||||
@@ -1292,11 +1334,7 @@ describe("short-term promotion", () => {
|
||||
expect(firstApply.applied).toBe(1);
|
||||
expect(firstApply.appended).toBe(1);
|
||||
|
||||
const rawStore = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
||||
for (const entry of Object.values(rawStore.entries)) {
|
||||
delete entry.promotedAt;
|
||||
}
|
||||
await testing.writeRawRecallStore(workspaceDir, rawStore);
|
||||
await clearPromotedAt(workspaceDir);
|
||||
|
||||
const secondApply = await applyAllCandidates(workspaceDir, ranked);
|
||||
expect(secondApply.applied).toBe(1);
|
||||
@@ -1588,31 +1626,7 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
|
||||
it("applies promotion candidates to MEMORY.md and marks them promoted", 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 recordMemoryRecalls(workspaceDir, "gateway host", [
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-01.md",
|
||||
10,
|
||||
12,
|
||||
0.92,
|
||||
"Gateway binds loopback and port 18789",
|
||||
),
|
||||
]);
|
||||
|
||||
const ranked = await rankAllCandidates(workspaceDir);
|
||||
const ranked = await seedGatewayPromotionCandidate(workspaceDir);
|
||||
const applied = await applyAllCandidates(workspaceDir, ranked);
|
||||
expect(applied.applied).toBe(1);
|
||||
|
||||
@@ -1728,31 +1742,7 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
|
||||
it("does not re-append candidates that were promoted in a prior run", 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 recordMemoryRecalls(workspaceDir, "gateway host", [
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-01.md",
|
||||
10,
|
||||
12,
|
||||
0.92,
|
||||
"Gateway binds loopback and port 18789",
|
||||
),
|
||||
]);
|
||||
|
||||
const ranked = await rankAllCandidates(workspaceDir);
|
||||
const ranked = await seedGatewayPromotionCandidate(workspaceDir);
|
||||
const first = await applyAllCandidates(workspaceDir, ranked);
|
||||
expect(first.applied).toBe(1);
|
||||
|
||||
@@ -2213,31 +2203,16 @@ describe("short-term promotion", () => {
|
||||
it("audits and repairs dangling recall entries and their phase signals", async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Live source note."]);
|
||||
await fs.mkdir(path.join(workspaceDir, "memory", "2026-04-02.md"));
|
||||
const buildEntry = (key: string, entryPath: string) => ({
|
||||
key,
|
||||
path: entryPath,
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory" as const,
|
||||
snippet: `${key} recall`,
|
||||
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: [],
|
||||
});
|
||||
await testing.writeRawRecallStore(workspaceDir, {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
entries: {
|
||||
live: buildEntry("live", "memory/2026-04-01.md"),
|
||||
directory: buildEntry("directory", "memory/2026-04-02.md"),
|
||||
missing: buildEntry("missing", "memory/2026-04-03.md"),
|
||||
live: recallStoreEntryFixture({ key: "live", path: "memory/2026-04-01.md" }),
|
||||
directory: recallStoreEntryFixture({
|
||||
key: "directory",
|
||||
path: "memory/2026-04-02.md",
|
||||
}),
|
||||
missing: recallStoreEntryFixture({ key: "missing", path: "memory/2026-04-03.md" }),
|
||||
},
|
||||
});
|
||||
await testing.writeRawPhaseSignalStore(workspaceDir, {
|
||||
@@ -2279,24 +2254,11 @@ describe("short-term promotion", () => {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
entries: {
|
||||
missing: {
|
||||
missing: recallStoreEntryFixture({
|
||||
key: "missing",
|
||||
path: "memory/2026-04-03.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory",
|
||||
snippet: "Missing source recall",
|
||||
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: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const nowIso = "2026-04-05T00:00:00.000Z";
|
||||
@@ -2320,30 +2282,12 @@ describe("short-term promotion", () => {
|
||||
|
||||
it("converges on retry when the recall write fails after phase cleanup", async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Live source note."]);
|
||||
const buildEntry = (key: string, entryPath: string) => ({
|
||||
key,
|
||||
path: entryPath,
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory" as const,
|
||||
snippet: `${key} recall`,
|
||||
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: [],
|
||||
});
|
||||
await testing.writeRawRecallStore(workspaceDir, {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
entries: {
|
||||
live: buildEntry("live", "memory/2026-04-01.md"),
|
||||
missing: buildEntry("missing", "memory/2026-04-03.md"),
|
||||
live: recallStoreEntryFixture({ key: "live", path: "memory/2026-04-01.md" }),
|
||||
missing: recallStoreEntryFixture({ key: "missing", path: "memory/2026-04-03.md" }),
|
||||
},
|
||||
});
|
||||
await testing.writeRawPhaseSignalStore(workspaceDir, {
|
||||
@@ -2396,24 +2340,11 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
|
||||
it("fails closed without changing recall state when source inspection is denied", async (workspaceDir) => {
|
||||
const entry = {
|
||||
const entry = recallStoreEntryFixture({
|
||||
key: "protected",
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory" as const,
|
||||
snippet: "Protected source recall",
|
||||
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: [],
|
||||
};
|
||||
});
|
||||
await testing.writeRawRecallStore(workspaceDir, {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
@@ -2447,24 +2378,19 @@ describe("short-term promotion", () => {
|
||||
entries: Object.fromEntries(
|
||||
Array.from({ length: maxEntries + 3 }, (_, index) => [
|
||||
`entry-${index}`,
|
||||
{
|
||||
recallStoreEntryFixture({
|
||||
key: `entry-${index}`,
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: index + 1,
|
||||
endLine: index + 1,
|
||||
source: "memory",
|
||||
snippet: `Oversized recall ${index} ${"x".repeat(maxSnippetChars + 100)}`,
|
||||
recallCount: 1,
|
||||
dailyCount: 0,
|
||||
groundedCount: 0,
|
||||
totalScore: index,
|
||||
maxScore: 0.75,
|
||||
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
||||
lastRecalledAt: new Date(Date.parse("2026-04-01T00:00:00.000Z") + index).toISOString(),
|
||||
queryHashes: [`q-${index}`],
|
||||
recallDays: ["2026-04-01"],
|
||||
conceptTags: [],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
),
|
||||
});
|
||||
@@ -2493,24 +2419,18 @@ describe("short-term promotion", () => {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-01T10:00:00.000Z",
|
||||
entries: {
|
||||
[key]: {
|
||||
[key]: recallStoreEntryFixture({
|
||||
key,
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory",
|
||||
snippet: "The owner prefers green tea.",
|
||||
recallCount: 1,
|
||||
dailyCount: 0,
|
||||
groundedCount: 0,
|
||||
totalScore: 0.8,
|
||||
maxScore: 0.8,
|
||||
firstRecalledAt: "2026-04-01T10:00:00.000Z",
|
||||
lastRecalledAt: "2026-04-01T10:00:00.000Z",
|
||||
queryHashes: ["legacy"],
|
||||
recallDays: ["2026-04-01"],
|
||||
conceptTags: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const legacy = await testing.readRecallStore(workspaceDir, "2026-04-01T10:00:00.000Z");
|
||||
@@ -2553,24 +2473,17 @@ describe("short-term promotion", () => {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
entries: {
|
||||
contaminated: {
|
||||
contaminated: recallStoreEntryFixture({
|
||||
key: "contaminated",
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory",
|
||||
snippet: `Candidate: ${"x".repeat(maxSnippetChars + 100)} confidence: 9 evidence: memory/.dreams/session-corpus/2026-04-01.txt status: staged recalls: 1`,
|
||||
recallCount: 1,
|
||||
dailyCount: 0,
|
||||
groundedCount: 0,
|
||||
totalScore: 1,
|
||||
maxScore: 0.75,
|
||||
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
||||
lastRecalledAt: "2026-04-01T00:00:00.000Z",
|
||||
queryHashes: ["q"],
|
||||
recallDays: ["2026-04-01"],
|
||||
conceptTags: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2598,22 +2511,11 @@ describe("short-term promotion", () => {
|
||||
version: 1,
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
entries: {
|
||||
good: {
|
||||
good: recallStoreEntryFixture({
|
||||
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: deriveConceptTags({
|
||||
path: "memory/2026-04-01.md",
|
||||
snippet,
|
||||
@@ -2623,7 +2525,7 @@ describe("short-term promotion", () => {
|
||||
sessionKind: "unknown",
|
||||
observedAt: Date.parse("2026-04-04T00:00:00.000Z"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
await testing.writeRawRecallStore(workspaceDir, raw);
|
||||
@@ -2769,50 +2671,51 @@ describe("short-term promotion", () => {
|
||||
describe("MEMORY.md budget compaction (#73691)", () => {
|
||||
async function applyBudgetCompactionPromotion(workspaceDir: string) {
|
||||
const nowMs = Date.parse("2026-04-29T10:00:00.000Z");
|
||||
await recordMemoryRecalls(
|
||||
workspaceDir,
|
||||
"rotate creds",
|
||||
[
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-29.md",
|
||||
3,
|
||||
3,
|
||||
0.96,
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
),
|
||||
],
|
||||
{ nowMs },
|
||||
);
|
||||
await recordRotateCredentialsRecall(workspaceDir);
|
||||
return await applyAllCandidates(workspaceDir, await rankAllCandidates(workspaceDir), {
|
||||
nowMs,
|
||||
memoryFileMaxChars: 1_400,
|
||||
});
|
||||
}
|
||||
|
||||
it("preserves mixed marker-backed user text during a real promotion write", async (workspaceDir) => {
|
||||
async function seedBudgetMemory(
|
||||
workspaceDir: string,
|
||||
firstMarker: string,
|
||||
secondMarker: string,
|
||||
between: string[] = [],
|
||||
): Promise<string> {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-29", [
|
||||
"Notes",
|
||||
"",
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
]);
|
||||
|
||||
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
||||
const filler = "x".repeat(600);
|
||||
const seeded = [
|
||||
"# Long-Term Memory",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-10)",
|
||||
"<!-- openclaw-memory-promotion:legacy-mixed -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
await fs.writeFile(
|
||||
memoryPath,
|
||||
[
|
||||
"# Long-Term Memory",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-10)",
|
||||
`<!-- openclaw-memory-promotion:${firstMarker} -->`,
|
||||
`- ${filler}`,
|
||||
"",
|
||||
...between,
|
||||
"## Promoted From Short-Term Memory (2026-04-20)",
|
||||
`<!-- openclaw-memory-promotion:${secondMarker} -->`,
|
||||
`- ${filler}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
return memoryPath;
|
||||
}
|
||||
|
||||
it("preserves mixed marker-backed user text during a real promotion write", async (workspaceDir) => {
|
||||
const memoryPath = await seedBudgetMemory(workspaceDir, "legacy-mixed", "legacy-generated", [
|
||||
"USER-AUTHORED: recovery key is paper-copy-17",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-20)",
|
||||
"<!-- openclaw-memory-promotion:legacy-generated -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
].join("\n");
|
||||
await fs.writeFile(memoryPath, seeded, "utf-8");
|
||||
]);
|
||||
|
||||
const applied = await applyBudgetCompactionPromotion(workspaceDir);
|
||||
|
||||
@@ -2826,30 +2729,11 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
|
||||
it("preserves an indented user ATX heading when compaction writes MEMORY.md", async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-29", [
|
||||
"Notes",
|
||||
"",
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
]);
|
||||
|
||||
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
||||
const filler = "x".repeat(600);
|
||||
const seeded = [
|
||||
"# Long-Term Memory",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-10)",
|
||||
"<!-- openclaw-memory-promotion:legacy-old -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
const memoryPath = await seedBudgetMemory(workspaceDir, "legacy-old", "legacy-newer", [
|
||||
" ### Correction (added by me)",
|
||||
"The prod DB is db-2.corp.example, NOT db-1.",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-20)",
|
||||
"<!-- openclaw-memory-promotion:legacy-newer -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
].join("\n");
|
||||
await fs.writeFile(memoryPath, seeded, "utf-8");
|
||||
]);
|
||||
|
||||
const applied = await applyBudgetCompactionPromotion(workspaceDir);
|
||||
|
||||
@@ -2863,29 +2747,8 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
|
||||
it("drops the oldest promoted section before write when memoryFileMaxChars would be exceeded", async (workspaceDir) => {
|
||||
// Source daily note that the candidate references (rehydrate reads it).
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-29", [
|
||||
"Notes",
|
||||
"",
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
]);
|
||||
|
||||
// Seed an oversized MEMORY.md with two pre-existing promotion sections.
|
||||
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
||||
const filler = "x".repeat(600);
|
||||
const seeded = [
|
||||
"# Long-Term Memory",
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-10)",
|
||||
"<!-- openclaw-memory-promotion:legacy-old -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
"## Promoted From Short-Term Memory (2026-04-20)",
|
||||
"<!-- openclaw-memory-promotion:legacy-newer -->",
|
||||
`- ${filler}`,
|
||||
"",
|
||||
].join("\n");
|
||||
await fs.writeFile(memoryPath, seeded, "utf-8");
|
||||
const memoryPath = await seedBudgetMemory(workspaceDir, "legacy-old", "legacy-newer");
|
||||
|
||||
const applied = await applyBudgetCompactionPromotion(workspaceDir);
|
||||
|
||||
@@ -3103,20 +2966,7 @@ describe("short-term promotion", () => {
|
||||
const seeded = `# Long-Term Memory\n\n${filler}\n- ${sentinel}\n`;
|
||||
await fs.writeFile(memoryPath, seeded, "utf-8");
|
||||
|
||||
await recordMemoryRecalls(
|
||||
workspaceDir,
|
||||
"rotate creds",
|
||||
[
|
||||
memoryRecallResult(
|
||||
"memory/2026-04-29.md",
|
||||
3,
|
||||
3,
|
||||
0.96,
|
||||
"Rotate the staging Postgres credentials before next deploy.",
|
||||
),
|
||||
],
|
||||
{ nowMs: Date.parse("2026-04-29T10:00:00.000Z") },
|
||||
);
|
||||
await recordRotateCredentialsRecall(workspaceDir);
|
||||
|
||||
const ranked = await rankAllCandidates(workspaceDir);
|
||||
|
||||
|
||||
@@ -251,6 +251,19 @@ function materializeRegisteredTool(
|
||||
: toolOrFactory;
|
||||
}
|
||||
|
||||
function registeredTool(
|
||||
registerTool: ReturnType<typeof vi.fn>,
|
||||
name: string,
|
||||
context: Record<string, unknown> = {},
|
||||
) {
|
||||
const factory = registerTool.mock.calls.find(([, options]) => options?.name === name)?.[0];
|
||||
const tool = materializeRegisteredTool(factory, context);
|
||||
if (!tool) {
|
||||
throw new Error(`expected ${name} tool registration`);
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
function createAgentScopedSchemaMock() {
|
||||
return vi.fn(async () => ({ fields: [{ name: "agentId" }] }));
|
||||
}
|
||||
@@ -263,6 +276,43 @@ function createAgentScopedVectorQuery(limit: ReturnType<typeof vi.fn>) {
|
||||
};
|
||||
}
|
||||
|
||||
function createStandardMemoryTableHarness(
|
||||
options: {
|
||||
toArray?: ReturnType<typeof vi.fn>;
|
||||
limit?: ReturnType<typeof vi.fn>;
|
||||
vectorSearch?: ReturnType<typeof vi.fn>;
|
||||
countRows?: ReturnType<typeof vi.fn>;
|
||||
add?: ReturnType<typeof vi.fn>;
|
||||
deleteRows?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
const toArray = options.toArray ?? vi.fn(async () => []);
|
||||
const limit = options.limit ?? vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = options.vectorSearch ?? vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const countRows = options.countRows ?? vi.fn(async () => 0);
|
||||
const add = options.add ?? vi.fn(async () => undefined);
|
||||
const deleteRows = options.deleteRows ?? vi.fn(async () => undefined);
|
||||
const openTable = vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows,
|
||||
add,
|
||||
delete: deleteRows,
|
||||
}));
|
||||
const connect = vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable,
|
||||
}));
|
||||
const module = { connect };
|
||||
return {
|
||||
add,
|
||||
limit,
|
||||
loadLanceDbModule: vi.fn(async () => module),
|
||||
module,
|
||||
vectorSearch,
|
||||
};
|
||||
}
|
||||
|
||||
function firstAddedMemory(add: ReturnType<typeof vi.fn>) {
|
||||
const batch = firstMockArg(add as MockCallSource, "memory add") as
|
||||
| Array<Record<string, unknown>>
|
||||
@@ -369,6 +419,19 @@ describe("memory plugin e2e", () => {
|
||||
}) as MemoryPluginTestConfig | undefined;
|
||||
}
|
||||
|
||||
function createPluginConfig(overrides: Partial<MemoryPluginTestConfig> = {}) {
|
||||
return {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
...overrides,
|
||||
} satisfies MemoryPluginTestConfig;
|
||||
}
|
||||
|
||||
function setupMemoryHookHarness(options: {
|
||||
autoCapture: boolean;
|
||||
autoRecall: boolean;
|
||||
@@ -379,32 +442,14 @@ describe("memory plugin e2e", () => {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const add = vi.fn(async () => undefined);
|
||||
const toArray = vi.fn(async () => options.searchResults ?? []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const openTable = vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add,
|
||||
delete: vi.fn(async () => undefined),
|
||||
}));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable,
|
||||
})),
|
||||
}));
|
||||
const pluginConfig: MemoryPluginTestConfig = {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
const { add, loadLanceDbModule, vectorSearch } = createStandardMemoryTableHarness({
|
||||
toArray,
|
||||
});
|
||||
const pluginConfig = createPluginConfig({
|
||||
autoCapture: options.autoCapture,
|
||||
autoRecall: options.autoRecall,
|
||||
};
|
||||
});
|
||||
let configFile: Record<string, unknown> = {
|
||||
plugins: { entries: { "memory-lancedb": { config: pluginConfig } } },
|
||||
};
|
||||
@@ -537,12 +582,7 @@ describe("memory plugin e2e", () => {
|
||||
|
||||
test("registers as disabled instead of throwing when inspected without config", () => {
|
||||
const registerService = vi.fn();
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
const logger = createTestLogger();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {},
|
||||
logger,
|
||||
@@ -565,15 +605,10 @@ describe("memory plugin e2e", () => {
|
||||
test("registers auto-recall on before_prompt_build instead of the legacy hook", () => {
|
||||
const on = vi.fn();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
}),
|
||||
on,
|
||||
});
|
||||
|
||||
@@ -944,18 +979,7 @@ describe("memory plugin e2e", () => {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const toArray = vi.fn(async () => []);
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch: vi.fn(() => createAgentScopedVectorQuery(vi.fn(() => ({ toArray })))),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const { loadLanceDbModule } = createStandardMemoryTableHarness({ toArray });
|
||||
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
ensureGlobalUndiciEnvProxyDispatcher: vi.fn(),
|
||||
@@ -1054,40 +1078,17 @@ describe("memory plugin e2e", () => {
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const toArray = vi.fn(async () => []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const { limit, loadLanceDbModule } = createStandardMemoryTableHarness({ toArray });
|
||||
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
embeddingsCreate,
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const registeredTools: any[] = [];
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
const mockApi = createMemoryPluginApi(getDbPath());
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
|
||||
await recallTool.execute("test-call-string-limit", {
|
||||
query: "project memory",
|
||||
@@ -1143,37 +1144,18 @@ describe("memory plugin e2e", () => {
|
||||
_distance: 0.2,
|
||||
},
|
||||
]);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const { limit, loadLanceDbModule } = createStandardMemoryTableHarness({ toArray });
|
||||
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
embeddingsCreate,
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const registeredTools: any[] = [];
|
||||
const pluginConfig = {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
const pluginConfig = createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
recallMaxChars: 1000,
|
||||
};
|
||||
});
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig,
|
||||
runtime: {
|
||||
@@ -1189,18 +1171,10 @@ describe("memory plugin e2e", () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
|
||||
const result = await recallTool.execute("test-call-untrusted-recall", {
|
||||
query: "stored instructions",
|
||||
@@ -1274,27 +1248,13 @@ describe("memory plugin e2e", () => {
|
||||
openAiPost: post,
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const registeredTools: any[] = [];
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
const logger = createTestLogger();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
logger,
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
|
||||
const resultPromise = recallTool.execute("timeout-call", { query: "project memory" });
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
@@ -1379,15 +1339,10 @@ describe("memory plugin e2e", () => {
|
||||
test("keeps before_prompt_build registered but inert when auto-recall is disabled", async () => {
|
||||
const on = vi.fn();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
}),
|
||||
on,
|
||||
});
|
||||
|
||||
@@ -1409,15 +1364,10 @@ describe("memory plugin e2e", () => {
|
||||
test("keeps agent_end registered but inert when auto-capture is disabled", async () => {
|
||||
const on = vi.fn();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
}),
|
||||
on,
|
||||
});
|
||||
|
||||
@@ -1452,15 +1402,10 @@ describe("memory plugin e2e", () => {
|
||||
run: async () => {
|
||||
const on = vi.fn();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
}),
|
||||
on,
|
||||
});
|
||||
|
||||
@@ -1507,21 +1452,9 @@ describe("memory plugin e2e", () => {
|
||||
_distance: 0.1,
|
||||
},
|
||||
]);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const openTable = vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
}));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable,
|
||||
})),
|
||||
}));
|
||||
const { limit, loadLanceDbModule, vectorSearch } = createStandardMemoryTableHarness({
|
||||
toArray,
|
||||
});
|
||||
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
@@ -1529,23 +1462,13 @@ describe("memory plugin e2e", () => {
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const on = vi.fn();
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
const logger = createTestLogger();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
recallMaxChars: 120,
|
||||
},
|
||||
}),
|
||||
logger,
|
||||
on,
|
||||
});
|
||||
@@ -1616,18 +1539,7 @@ describe("memory plugin e2e", () => {
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const toArray = vi.fn(() => new Promise(() => {}));
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch: vi.fn(() => createAgentScopedVectorQuery(limit)),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const { loadLanceDbModule } = createStandardMemoryTableHarness({ limit });
|
||||
|
||||
try {
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
@@ -1636,27 +1548,13 @@ describe("memory plugin e2e", () => {
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const on = vi.fn();
|
||||
const registeredTools: any[] = [];
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
const logger = createTestLogger();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
}),
|
||||
logger,
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
on,
|
||||
});
|
||||
|
||||
@@ -1696,12 +1594,7 @@ describe("memory plugin e2e", () => {
|
||||
"memory-lancedb: auto-recall skipped during recall cooldown: auto-recall timed out after 15s",
|
||||
);
|
||||
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((tool) => tool.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
const toolResult = await recallTool.execute("cooldown-call", { query: "editor" });
|
||||
expect(toolResult.details).toMatchObject({
|
||||
count: 0,
|
||||
@@ -2322,23 +2215,8 @@ describe("memory plugin e2e", () => {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const add = vi.fn(async () => undefined);
|
||||
const toArray = vi.fn(async () => overrides?.searchResults ?? []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const openTable = vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add,
|
||||
delete: vi.fn(async () => undefined),
|
||||
}));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable,
|
||||
})),
|
||||
}));
|
||||
const { add, loadLanceDbModule } = createStandardMemoryTableHarness({ toArray });
|
||||
|
||||
installOpenAiMemoryModuleMocks({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
@@ -2347,22 +2225,12 @@ describe("memory plugin e2e", () => {
|
||||
});
|
||||
|
||||
const on = vi.fn();
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
const logger = createTestLogger();
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
}),
|
||||
logger,
|
||||
on,
|
||||
});
|
||||
@@ -2665,18 +2533,7 @@ describe("memory plugin e2e", () => {
|
||||
const toArray = vi.fn(async () => []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn((_vector?: number[]) => createAgentScopedVectorQuery(limit));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const { loadLanceDbModule } = createStandardMemoryTableHarness({ limit, vectorSearch });
|
||||
|
||||
const post = vi.fn((_path: string, opts: { body?: unknown }) =>
|
||||
invokeEmbeddingCreate(embeddingsCreate, opts.body),
|
||||
@@ -2688,30 +2545,20 @@ describe("memory plugin e2e", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const registeredTools: any[] = [];
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
pluginConfig: createPluginConfig({
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
dimensions: 1024,
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
},
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
await recallTool.execute("test-call-dims", { query: "hello dimensions" });
|
||||
|
||||
expect(loadLanceDbModule).toHaveBeenCalledTimes(1);
|
||||
@@ -2753,23 +2600,11 @@ describe("memory plugin e2e", () => {
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const toArray = vi.fn(async () => []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const { module } = createStandardMemoryTableHarness({ toArray });
|
||||
const loadLanceDbModule = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("temporary LanceDB install failure"))
|
||||
.mockResolvedValueOnce({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
.mockResolvedValueOnce(module);
|
||||
|
||||
installOpenAiMemoryModuleMocks({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
@@ -2778,20 +2613,10 @@ describe("memory plugin e2e", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const registeredTools: any[] = [];
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
const mockApi = createMemoryPluginApi(getDbPath());
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const recallTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool,
|
||||
);
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
const recallTool = registeredTool(mockApi.registerTool, "memory_recall");
|
||||
|
||||
await expect(recallTool.execute("test-call-retry-1", { query: "hello" })).rejects.toThrow(
|
||||
"temporary LanceDB install failure",
|
||||
@@ -3124,38 +2949,18 @@ describe("memory plugin e2e", () => {
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const add = vi.fn(async () => undefined);
|
||||
const toArray = vi.fn(async (): Promise<Record<string, unknown>[]> => []);
|
||||
const limit = vi.fn(() => ({ toArray }));
|
||||
const vectorSearch = vi.fn(() => createAgentScopedVectorQuery(limit));
|
||||
const openTable = vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
add,
|
||||
countRows: vi.fn(async () => 0),
|
||||
delete: vi.fn(async () => undefined),
|
||||
}));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable,
|
||||
})),
|
||||
}));
|
||||
const { loadLanceDbModule } = createStandardMemoryTableHarness({ add, toArray });
|
||||
|
||||
await withMockedOpenAiMemoryPlugin({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
embeddingsCreate,
|
||||
loadLanceDbModule,
|
||||
run: async () => {
|
||||
const registeredTools: any[] = [];
|
||||
const pluginConfig = {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
const pluginConfig = createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
captureMaxChars: 1000,
|
||||
};
|
||||
});
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig,
|
||||
runtime: {
|
||||
@@ -3171,24 +2976,15 @@ describe("memory plugin e2e", () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const storeTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_store")?.tool,
|
||||
);
|
||||
if (!storeTool) {
|
||||
throw new Error("memory_store tool was not registered");
|
||||
}
|
||||
const storeTool = registeredTool(mockApi.registerTool, "memory_store");
|
||||
expect(storeTool.description).toContain("does not guarantee semantic recall");
|
||||
|
||||
const incognitoStoreTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_store")?.tool,
|
||||
{ sessionKey: "agent:main:internal-session-effects:incognito-memory-test" },
|
||||
);
|
||||
const incognitoStoreTool = registeredTool(mockApi.registerTool, "memory_store", {
|
||||
sessionKey: "agent:main:internal-session-effects:incognito-memory-test",
|
||||
});
|
||||
const incognitoRejected = await incognitoStoreTool.execute("test-call-incognito", {
|
||||
text: "The user prefers concise replies",
|
||||
});
|
||||
@@ -3348,23 +3144,15 @@ describe("memory plugin e2e", () => {
|
||||
})),
|
||||
}),
|
||||
run: async () => {
|
||||
const registeredTools: any[] = [];
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
pluginConfig: {
|
||||
embedding: { apiKey: OPENAI_API_KEY, model: "text-embedding-3-small" },
|
||||
dbPath: getDbPath(),
|
||||
pluginConfig: createPluginConfig({
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
recallMaxChars: 100,
|
||||
},
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
}),
|
||||
});
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const forgetTool = materializeRegisteredTool(
|
||||
registeredTools.find((entry) => entry.opts?.name === "memory_forget")?.tool,
|
||||
);
|
||||
const forgetTool = registeredTool(mockApi.registerTool, "memory_forget");
|
||||
expectToolExecute(forgetTool, "memory_forget");
|
||||
|
||||
const directAbsent = await forgetTool.execute("forget-direct-absent", { memoryId });
|
||||
@@ -3483,18 +3271,13 @@ describe("memory plugin e2e", () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
schema: createAgentScopedSchemaMock(),
|
||||
vectorSearch,
|
||||
countRows: vi.fn(async () => 2),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const countRows = vi.fn(async () => 2);
|
||||
const { loadLanceDbModule } = createStandardMemoryTableHarness({
|
||||
toArray,
|
||||
limit: limitFn,
|
||||
vectorSearch,
|
||||
countRows,
|
||||
});
|
||||
installOpenAiMemoryModuleMocks({
|
||||
ensureGlobalUndiciEnvProxyDispatcher: vi.fn(),
|
||||
embeddingsCreate,
|
||||
@@ -3502,20 +3285,10 @@ describe("memory plugin e2e", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const registeredTools: any[] = [];
|
||||
const mockApi = createMemoryPluginApi(getDbPath(), {
|
||||
registerTool: (tool: any, opts: any) => {
|
||||
registeredTools.push({ tool, opts });
|
||||
},
|
||||
});
|
||||
const mockApi = createMemoryPluginApi(getDbPath());
|
||||
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const forgetTool = materializeRegisteredTool(
|
||||
registeredTools.find((t) => t.opts?.name === "memory_forget")?.tool,
|
||||
);
|
||||
if (!forgetTool) {
|
||||
throw new Error("expected memory_forget tool registration");
|
||||
}
|
||||
const forgetTool = registeredTool(mockApi.registerTool, "memory_forget");
|
||||
expectToolExecute(forgetTool);
|
||||
|
||||
const result = await forgetTool.execute("test-call-full-ids", { query: "user preference" });
|
||||
|
||||
@@ -2,21 +2,16 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
tempWorkspace,
|
||||
type TempWorkspace,
|
||||
} from "openclaw/plugin-sdk/temp-path";
|
||||
import {
|
||||
createSandboxBrowserConfig,
|
||||
createSandboxPruneConfig,
|
||||
createSandboxSshConfig,
|
||||
createSandboxTestContext,
|
||||
} from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { createSandboxTestContext } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createOpenShellSandboxBackendFactory } from "./backend.js";
|
||||
import { resolveOpenShellPluginConfig } from "./config.js";
|
||||
import { createOpenShellBackendSandboxConfig } from "./openshell.test-support.js";
|
||||
|
||||
const sdkMocks = vi.hoisted(() => ({
|
||||
runSshSandboxCommand: vi.fn(),
|
||||
@@ -50,32 +45,6 @@ vi.mock("./cli.js", async (importOriginal) => {
|
||||
|
||||
const tempWorkspaces: TempWorkspace[] = [];
|
||||
|
||||
function createOpenShellBackendSandboxConfig(): CreateSandboxBackendParams["cfg"] {
|
||||
return {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
workspaceRoot: "/tmp/openclaw-sandboxes",
|
||||
dockerTmpfsSource: "configured",
|
||||
docker: {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
containerPrefix: "openclaw-sbx-",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: false,
|
||||
tmpfs: [],
|
||||
network: "none",
|
||||
capDrop: [],
|
||||
binds: [],
|
||||
env: {},
|
||||
},
|
||||
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
|
||||
browser: createSandboxBrowserConfig(),
|
||||
tools: { allow: ["*"], deny: [] },
|
||||
prune: createSandboxPruneConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
async function createOpenShellBackendFixture(params: {
|
||||
workspaceDir: string;
|
||||
scopeKey: string;
|
||||
@@ -152,18 +121,9 @@ describe("openshell backend exec workdir validation", () => {
|
||||
await fs.mkdir(protectedPath, { recursive: true });
|
||||
await fs.writeFile(path.join(protectedPath, "private.txt"), "host-only", "utf8");
|
||||
}
|
||||
const backendFactory = createOpenShellSandboxBackendFactory({
|
||||
pluginConfig: resolveOpenShellPluginConfig({
|
||||
command: "openshell",
|
||||
mode: "mirror",
|
||||
}),
|
||||
});
|
||||
const backend = await backendFactory({
|
||||
sessionKey: "agent:main:turn",
|
||||
const backend = await createOpenShellBackendFixture({
|
||||
scopeKey: "agent:somalley_alice:dashboard-8",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
cfg: createOpenShellBackendSandboxConfig(),
|
||||
});
|
||||
|
||||
await expect(backend.validateWorkdir?.("/workspace")).resolves.toBe("/workspace");
|
||||
@@ -241,18 +201,9 @@ describe("openshell backend exec workdir validation", () => {
|
||||
tempWorkspaces.push(workspace);
|
||||
const workspaceDir = workspace.dir;
|
||||
await fs.writeFile(path.join(workspaceDir, "seed.txt"), "seed", "utf8");
|
||||
const backendFactory = createOpenShellSandboxBackendFactory({
|
||||
pluginConfig: resolveOpenShellPluginConfig({
|
||||
command: "openshell",
|
||||
mode: "mirror",
|
||||
}),
|
||||
});
|
||||
const backend = await backendFactory({
|
||||
sessionKey: "agent:main:turn",
|
||||
const backend = await createOpenShellBackendFixture({
|
||||
scopeKey: "agent:main",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
cfg: createOpenShellBackendSandboxConfig(),
|
||||
});
|
||||
|
||||
await expect(backend.validateWorkdir?.("/workspace")).resolves.toBe("/workspace");
|
||||
|
||||
@@ -3,20 +3,15 @@
|
||||
// memory, and must never re-seed roots that already hold content.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
tempWorkspace,
|
||||
type TempWorkspace,
|
||||
} from "openclaw/plugin-sdk/temp-path";
|
||||
import {
|
||||
createSandboxBrowserConfig,
|
||||
createSandboxPruneConfig,
|
||||
createSandboxSshConfig,
|
||||
} from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createOpenShellSandboxBackendFactory } from "./backend.js";
|
||||
import { resolveOpenShellPluginConfig } from "./config.js";
|
||||
import { createOpenShellBackendSandboxConfig } from "./openshell.test-support.js";
|
||||
|
||||
const sdkMocks = vi.hoisted(() => ({
|
||||
runSshSandboxCommand: vi.fn(),
|
||||
@@ -50,32 +45,6 @@ vi.mock("./cli.js", async (importOriginal) => {
|
||||
|
||||
const tempWorkspaces: TempWorkspace[] = [];
|
||||
|
||||
function createOpenShellBackendSandboxConfig(): CreateSandboxBackendParams["cfg"] {
|
||||
return {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
workspaceRoot: "/tmp/openclaw-sandboxes",
|
||||
dockerTmpfsSource: "configured",
|
||||
docker: {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
containerPrefix: "openclaw-sbx-",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: false,
|
||||
tmpfs: [],
|
||||
network: "none",
|
||||
capDrop: [],
|
||||
binds: [],
|
||||
env: {},
|
||||
},
|
||||
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
|
||||
browser: createSandboxBrowserConfig(),
|
||||
tools: { allow: ["*"], deny: [] },
|
||||
prune: createSandboxPruneConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
async function createAdoptedRemoteBackend(params: { probeStdout: string }) {
|
||||
const workspace = await tempWorkspace({
|
||||
rootDir: resolvePreferredOpenClawTmpDir(),
|
||||
|
||||
@@ -7,19 +7,13 @@ import {
|
||||
buildExecRemoteCommand,
|
||||
disposeSshSandboxSession,
|
||||
shellEscape,
|
||||
type CreateSandboxBackendParams,
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
tempWorkspace,
|
||||
type TempWorkspace,
|
||||
} from "openclaw/plugin-sdk/temp-path";
|
||||
import {
|
||||
createSandboxBrowserConfig,
|
||||
createSandboxPruneConfig,
|
||||
createSandboxSshConfig,
|
||||
createSandboxTestContext,
|
||||
} from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { createSandboxTestContext } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenShellSandboxBackend } from "./backend.types.js";
|
||||
import {
|
||||
@@ -28,6 +22,10 @@ import {
|
||||
runOpenShellCli,
|
||||
} from "./cli.js";
|
||||
import { resolveOpenShellPluginConfig } from "./config.js";
|
||||
import {
|
||||
createOpenShellBackendSandboxConfig,
|
||||
createOpenShellRuntimeEntryFixture,
|
||||
} from "./openshell.test-support.js";
|
||||
|
||||
const openShellTestWorkspaceRoot = resolvePreferredOpenClawTmpDir();
|
||||
|
||||
@@ -522,16 +520,7 @@ describe("openshell backend manager", () => {
|
||||
});
|
||||
|
||||
const result = await manager.describeRuntime({
|
||||
entry: {
|
||||
containerName: "openclaw-session-1234",
|
||||
backendId: "openshell",
|
||||
runtimeLabel: "openclaw-session-1234",
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: "custom-source",
|
||||
configLabelKind: "Source",
|
||||
},
|
||||
entry: createOpenShellRuntimeEntryFixture("openclaw-session-1234", "custom-source"),
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
@@ -579,16 +568,7 @@ describe("openshell backend manager", () => {
|
||||
|
||||
await expect(
|
||||
manager.describeRuntime({
|
||||
entry: {
|
||||
containerName: "openclaw-session-1234",
|
||||
backendId: "openshell",
|
||||
runtimeLabel: "openclaw-session-1234",
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: "openclaw",
|
||||
configLabelKind: "Source",
|
||||
},
|
||||
entry: createOpenShellRuntimeEntryFixture("openclaw-session-1234"),
|
||||
config: {},
|
||||
}),
|
||||
).resolves.toMatchObject({ running: false });
|
||||
@@ -610,16 +590,7 @@ describe("openshell backend manager", () => {
|
||||
});
|
||||
|
||||
await manager.removeRuntime({
|
||||
entry: {
|
||||
containerName: "openclaw-session-5678",
|
||||
backendId: "openshell",
|
||||
runtimeLabel: "openclaw-session-5678",
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: "openclaw",
|
||||
configLabelKind: "Source",
|
||||
},
|
||||
entry: createOpenShellRuntimeEntryFixture("openclaw-session-5678"),
|
||||
config: {},
|
||||
});
|
||||
|
||||
@@ -636,16 +607,7 @@ describe("openshell backend manager", () => {
|
||||
});
|
||||
|
||||
await manager.removeRuntime({
|
||||
entry: {
|
||||
containerName: "openclaw-session-5678",
|
||||
backendId: "openshell",
|
||||
runtimeLabel: "openclaw-session-5678",
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: "openclaw",
|
||||
configLabelKind: "Source",
|
||||
},
|
||||
entry: createOpenShellRuntimeEntryFixture("openclaw-session-5678"),
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
@@ -691,16 +653,7 @@ describe("openshell backend manager", () => {
|
||||
|
||||
await expect(
|
||||
manager.removeRuntime({
|
||||
entry: {
|
||||
containerName: "openclaw-session-5678",
|
||||
backendId: "openshell",
|
||||
runtimeLabel: "openclaw-session-5678",
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: "openclaw",
|
||||
configLabelKind: "Source",
|
||||
},
|
||||
entry: createOpenShellRuntimeEntryFixture("openclaw-session-5678"),
|
||||
config: {},
|
||||
}),
|
||||
).rejects.toThrow(expected);
|
||||
@@ -929,32 +882,6 @@ describe("openshell backend manager", () => {
|
||||
|
||||
const executableWorkspaces: TempWorkspace[] = [];
|
||||
|
||||
function createOpenShellBackendSandboxConfig(): CreateSandboxBackendParams["cfg"] {
|
||||
return {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
workspaceRoot: "/tmp/openclaw-sandboxes",
|
||||
dockerTmpfsSource: "configured",
|
||||
docker: {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
containerPrefix: "openclaw-sbx-",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: false,
|
||||
tmpfs: [],
|
||||
network: "none",
|
||||
capDrop: [],
|
||||
binds: [],
|
||||
env: {},
|
||||
},
|
||||
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
|
||||
browser: createSandboxBrowserConfig(),
|
||||
tools: { allow: ["*"], deny: [] },
|
||||
prune: createSandboxPruneConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
async function makeExecutable(params: { name: string; script: string }): Promise<string> {
|
||||
const workspace = await createOpenShellTestWorkspace("bin");
|
||||
executableWorkspaces.push(workspace);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
createSandboxBrowserConfig,
|
||||
createSandboxPruneConfig,
|
||||
createSandboxSshConfig,
|
||||
} from "openclaw/plugin-sdk/test-fixtures";
|
||||
|
||||
export function createOpenShellBackendSandboxConfig(): CreateSandboxBackendParams["cfg"] {
|
||||
return {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
workspaceRoot: "/tmp/openclaw-sandboxes",
|
||||
dockerTmpfsSource: "configured",
|
||||
docker: {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
containerPrefix: "openclaw-sbx-",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: false,
|
||||
tmpfs: [],
|
||||
network: "none",
|
||||
capDrop: [],
|
||||
binds: [],
|
||||
env: {},
|
||||
},
|
||||
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
|
||||
browser: createSandboxBrowserConfig(),
|
||||
tools: { allow: ["*"], deny: [] },
|
||||
prune: createSandboxPruneConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createOpenShellRuntimeEntryFixture(runtimeId: string, configLabel = "openclaw") {
|
||||
return {
|
||||
containerName: runtimeId,
|
||||
backendId: "openshell",
|
||||
runtimeLabel: runtimeId,
|
||||
sessionKey: "agent:main",
|
||||
createdAtMs: 1,
|
||||
lastUsedAtMs: 1,
|
||||
image: configLabel,
|
||||
configLabelKind: "Source",
|
||||
} as const;
|
||||
}
|
||||
+242
-538
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user