






















@@ -1271,6 +1271,232 @@ describe("memory plugin e2e", () => {
12711271}
12721272});
127312731274+async function setupAutoCaptureCursorHarness(overrides?: {
1275+embeddingsCreate?: ReturnType<typeof vi.fn>;
1276+searchResults?: Array<Record<string, unknown>>;
1277+}) {
1278+const embeddingsCreate =
1279+overrides?.embeddingsCreate ??
1280+vi.fn(async () => ({
1281+data: [{ embedding: [0.1, 0.2, 0.3] }],
1282+}));
1283+const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
1284+const add = vi.fn(async () => undefined);
1285+const toArray = vi.fn(async () => overrides?.searchResults ?? []);
1286+const limit = vi.fn(() => ({ toArray }));
1287+const vectorSearch = vi.fn(() => ({ limit }));
1288+const openTable = vi.fn(async () => ({
1289+ vectorSearch,
1290+countRows: vi.fn(async () => 0),
1291+ add,
1292+delete: vi.fn(async () => undefined),
1293+}));
1294+const loadLanceDbModule = vi.fn(async () => ({
1295+connect: vi.fn(async () => ({
1296+tableNames: vi.fn(async () => ["memories"]),
1297+ openTable,
1298+})),
1299+}));
1300+1301+vi.resetModules();
1302+vi.doMock("openclaw/plugin-sdk/runtime-env", () => ({
1303+ ensureGlobalUndiciEnvProxyDispatcher,
1304+}));
1305+vi.doMock("openai", () => ({
1306+default: class MockOpenAI {
1307+embeddings = { create: embeddingsCreate };
1308+},
1309+}));
1310+vi.doMock("./lancedb-runtime.js", () => ({
1311+ loadLanceDbModule,
1312+}));
1313+1314+const { default: dynamicMemoryPlugin } = await import("./index.js");
1315+const on = vi.fn();
1316+const logger = {
1317+info: vi.fn(),
1318+warn: vi.fn(),
1319+error: vi.fn(),
1320+debug: vi.fn(),
1321+};
1322+const mockApi = {
1323+id: "memory-lancedb",
1324+name: "Memory (LanceDB)",
1325+source: "test",
1326+config: {},
1327+pluginConfig: {
1328+embedding: {
1329+apiKey: OPENAI_API_KEY,
1330+model: "text-embedding-3-small",
1331+},
1332+dbPath: getDbPath(),
1333+autoCapture: true,
1334+autoRecall: false,
1335+},
1336+runtime: {},
1337+ logger,
1338+registerTool: vi.fn(),
1339+registerCli: vi.fn(),
1340+registerService: vi.fn(),
1341+ on,
1342+resolvePath: (p: string) => p,
1343+};
1344+1345+dynamicMemoryPlugin.register(mockApi as any);
1346+1347+const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
1348+const sessionEnd = on.mock.calls.find(([hookName]) => hookName === "session_end")?.[1];
1349+expect(agentEnd).toBeTypeOf("function");
1350+expect(sessionEnd).toBeTypeOf("function");
1351+1352+return {
1353+ add,
1354+ agentEnd,
1355+ embeddingsCreate,
1356+ ensureGlobalUndiciEnvProxyDispatcher,
1357+ loadLanceDbModule,
1358+ logger,
1359+ sessionEnd,
1360+};
1361+}
1362+1363+async function cleanupAutoCaptureCursorHarness() {
1364+vi.doUnmock("openclaw/plugin-sdk/runtime-env");
1365+vi.doUnmock("openai");
1366+vi.doUnmock("./lancedb-runtime.js");
1367+vi.resetModules();
1368+}
1369+1370+test("skips already-processed auto-capture messages by session cursor", async () => {
1371+const harness = await setupAutoCaptureCursorHarness();
1372+1373+try {
1374+await harness.agentEnd?.(
1375+{
1376+success: true,
1377+messages: [{ role: "user", content: "I prefer Helix for editing code every day." }],
1378+},
1379+{ sessionKey: "session-a" },
1380+);
1381+await harness.agentEnd?.(
1382+{
1383+success: true,
1384+messages: [
1385+{ role: "user", content: "I prefer Helix for editing code every day." },
1386+{ role: "user", content: "I prefer Fish for shell commands every day." },
1387+],
1388+},
1389+{ sessionKey: "session-a" },
1390+);
1391+1392+expect(harness.embeddingsCreate).toHaveBeenCalledTimes(2);
1393+expect(harness.embeddingsCreate).toHaveBeenNthCalledWith(1, {
1394+model: "text-embedding-3-small",
1395+input: "I prefer Helix for editing code every day.",
1396+encoding_format: "float",
1397+});
1398+expect(harness.embeddingsCreate).toHaveBeenNthCalledWith(2, {
1399+model: "text-embedding-3-small",
1400+input: "I prefer Fish for shell commands every day.",
1401+encoding_format: "float",
1402+});
1403+expect(harness.add).toHaveBeenCalledTimes(2);
1404+} finally {
1405+await cleanupAutoCaptureCursorHarness();
1406+}
1407+});
1408+1409+test("does not advance auto-capture cursor when message processing fails", async () => {
1410+const embeddingsCreate = vi
1411+.fn()
1412+.mockRejectedValueOnce(new Error("temporary embedding failure"))
1413+.mockResolvedValueOnce({ data: [{ embedding: [0.1, 0.2, 0.3] }] });
1414+const harness = await setupAutoCaptureCursorHarness({ embeddingsCreate });
1415+1416+try {
1417+const event = {
1418+success: true,
1419+messages: [{ role: "user", content: "I prefer Helix for editing code every day." }],
1420+};
1421+1422+await harness.agentEnd?.(event, { sessionKey: "session-failure" });
1423+await harness.agentEnd?.(event, { sessionKey: "session-failure" });
1424+1425+expect(embeddingsCreate).toHaveBeenCalledTimes(2);
1426+expect(harness.add).toHaveBeenCalledTimes(1);
1427+expect(harness.logger.warn).toHaveBeenCalledWith(
1428+expect.stringContaining("memory-lancedb: capture failed:"),
1429+);
1430+} finally {
1431+await cleanupAutoCaptureCursorHarness();
1432+}
1433+});
1434+1435+test("does not lose new auto-capture messages after history compaction rewrites prior turns", async () => {
1436+const harness = await setupAutoCaptureCursorHarness();
1437+1438+try {
1439+await harness.agentEnd?.(
1440+{
1441+success: true,
1442+messages: [
1443+{ role: "user", content: "I prefer Helix for editing code every day." },
1444+{ role: "user", content: "I prefer Fish for shell commands every day." },
1445+],
1446+},
1447+{ sessionKey: "session-compacted" },
1448+);
1449+await harness.agentEnd?.(
1450+{
1451+success: true,
1452+messages: [
1453+{ role: "assistant", content: "Earlier history was compacted." },
1454+{ role: "user", content: "I prefer Deno for small scripts every day." },
1455+],
1456+},
1457+{ sessionKey: "session-compacted" },
1458+);
1459+1460+expect(harness.embeddingsCreate).toHaveBeenCalledTimes(3);
1461+expect(harness.embeddingsCreate).toHaveBeenNthCalledWith(3, {
1462+model: "text-embedding-3-small",
1463+input: "I prefer Deno for small scripts every day.",
1464+encoding_format: "float",
1465+});
1466+expect(harness.add).toHaveBeenCalledTimes(3);
1467+} finally {
1468+await cleanupAutoCaptureCursorHarness();
1469+}
1470+});
1471+1472+test("evicts auto-capture cursor state on session end", async () => {
1473+const harness = await setupAutoCaptureCursorHarness();
1474+1475+try {
1476+const event = {
1477+success: true,
1478+messages: [{ role: "user", content: "I prefer Helix for editing code every day." }],
1479+};
1480+1481+await harness.agentEnd?.(event, { sessionKey: "session-ended" });
1482+await harness.sessionEnd?.(
1483+{
1484+sessionId: "session-id",
1485+sessionKey: "session-ended",
1486+messageCount: 1,
1487+reason: "deleted",
1488+},
1489+{ sessionId: "session-id", sessionKey: "session-ended" },
1490+);
1491+await harness.agentEnd?.(event, { sessionKey: "session-ended" });
1492+1493+expect(harness.embeddingsCreate).toHaveBeenCalledTimes(2);
1494+expect(harness.add).toHaveBeenCalledTimes(2);
1495+} finally {
1496+await cleanupAutoCaptureCursorHarness();
1497+}
1498+});
1499+12741500test("passes configured dimensions to OpenAI embeddings API", async () => {
12751501const embeddingsCreate = vi.fn(async () => ({
12761502data: [{ embedding: [0.1, 0.2, 0.3] }],
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。