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:
Peter Steinberger
2026-08-26 14:08:08 -07:00
committed by GitHub
parent f290d6bc81
commit b3db4758a3
7 changed files with 650 additions and 1431 deletions
@@ -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);
+129 -356
View File
@@ -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(),
+10 -83
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff