Files
openclaw/extensions/memory-lancedb/memory-cli.test.ts
Peter Steinberger 433bb3f954 fix(cli): make commands, completion, and JSON output reliable (#116033)
* fix(cli): make commands, completion, and JSON output reliable

* fix(cli): reconcile completion coverage with current main

* test(cli): keep test routing stable across isolation lanes
2026-07-31 16:20:34 -07:00

108 lines
3.2 KiB
TypeScript

import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "./api.js";
import type { Embeddings } from "./embeddings.js";
import type { MemoryDB } from "./lancedb-store.js";
import { registerMemoryCli } from "./memory-cli.js";
function createHarness(params?: { embedError?: unknown; closeError?: Error }) {
const registerCli = vi.fn();
const closeError = params?.closeError;
const close = closeError
? vi.fn(async () => {
throw closeError;
})
: vi.fn(async () => {});
const embed =
params && Object.hasOwn(params, "embedError")
? vi.fn(async () => {
throw params.embedError;
})
: vi.fn(async () => [0.1, 0.2]);
const embeddings: Embeddings = {
embed,
close,
};
const search = vi.fn(async () => []);
registerMemoryCli(
{ registerCli } as unknown as OpenClawPluginApi,
{ search } as unknown as MemoryDB,
embeddings,
() => "main",
undefined,
);
const registrar = registerCli.mock.calls[0]?.[0] as
| ((params: { program: Command }) => void)
| undefined;
if (!registrar) {
throw new Error("expected memory CLI registrar");
}
const program = new Command();
registrar({ program });
return { close, embed, program, search };
}
describe("memory-lancedb CLI embedding lifecycle", () => {
it("closes embeddings after search", async () => {
const harness = createHarness();
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
try {
await harness.program.parseAsync(["node", "openclaw", "ltm", "search", "hello"]);
} finally {
log.mockRestore();
}
expect(harness.embed).toHaveBeenCalledTimes(1);
expect(harness.search).toHaveBeenCalledTimes(1);
expect(harness.close).toHaveBeenCalledTimes(1);
});
it("closes embeddings without masking search failure", async () => {
const harness = createHarness({
embedError: new Error("embedding failed"),
closeError: new Error("close failed"),
});
await expect(
harness.program.parseAsync(["node", "openclaw", "ltm", "search", "hello"]),
).rejects.toThrow("embedding failed");
expect(harness.close).toHaveBeenCalledTimes(1);
});
it("rejects an invalid limit before generating an embedding", async () => {
const harness = createHarness();
await expect(
harness.program.parseAsync([
"node",
"openclaw",
"ltm",
"search",
"hello",
"--limit",
"5items",
]),
).rejects.toThrow("--limit must be a positive integer");
expect(harness.embed).not.toHaveBeenCalled();
expect(harness.search).not.toHaveBeenCalled();
expect(harness.close).toHaveBeenCalledTimes(1);
});
it("preserves a falsy search rejection over cleanup failure", async () => {
const harness = createHarness({
embedError: null,
closeError: new Error("close failed"),
});
const rejection = await harness.program
.parseAsync(["node", "openclaw", "ltm", "search", "hello"])
.then(
() => "resolved",
(err: unknown) => err,
);
expect(rejection).toBeNull();
expect(harness.close).toHaveBeenCalledTimes(1);
});
});