Files
openclaw/extensions/memory-wiki/src/gateway.test.ts
2026-04-29 20:17:37 +01:00

467 lines
13 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import {
applyMemoryWikiMutation,
normalizeMemoryWikiMutationInput,
type ApplyMemoryWikiMutation,
} from "./apply.js";
import { registerMemoryWikiGatewayMethods } from "./gateway.js";
import { listMemoryWikiImportInsights } from "./import-insights.js";
import { listMemoryWikiImportRuns } from "./import-runs.js";
import { ingestMemoryWikiSource } from "./ingest.js";
import { listMemoryWikiPalace } from "./memory-palace.js";
import { searchMemoryWiki } from "./query.js";
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import { resolveMemoryWikiStatus } from "./status.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
vi.mock("./apply.js", () => ({
applyMemoryWikiMutation: vi.fn(),
normalizeMemoryWikiMutationInput: vi.fn(),
}));
vi.mock("./compile.js", () => ({
compileMemoryWikiVault: vi.fn(),
}));
vi.mock("./ingest.js", () => ({
ingestMemoryWikiSource: vi.fn(),
}));
vi.mock("./import-insights.js", () => ({
listMemoryWikiImportInsights: vi.fn(),
}));
vi.mock("./import-runs.js", () => ({
listMemoryWikiImportRuns: vi.fn(),
}));
vi.mock("./lint.js", () => ({
lintMemoryWikiVault: vi.fn(),
}));
vi.mock("./memory-palace.js", () => ({
listMemoryWikiPalace: vi.fn(),
}));
vi.mock("./obsidian.js", () => ({
probeObsidianCli: vi.fn(),
runObsidianCommand: vi.fn(),
runObsidianDaily: vi.fn(),
runObsidianOpen: vi.fn(),
runObsidianSearch: vi.fn(),
}));
vi.mock("./query.js", () => ({
getMemoryWikiPage: vi.fn(),
searchMemoryWiki: vi.fn(),
WIKI_SEARCH_MODES: ["auto", "find-person", "route-question", "source-evidence", "raw-claim"],
}));
vi.mock("./source-sync.js", () => ({
syncMemoryWikiImportedSources: vi.fn(),
}));
vi.mock("./status.js", () => ({
buildMemoryWikiDoctorReport: vi.fn(),
resolveMemoryWikiStatus: vi.fn(),
}));
vi.mock("./vault.js", () => ({
initializeMemoryWikiVault: vi.fn(),
}));
const { createPluginApi, createVault } = createMemoryWikiTestHarness();
function findGatewayHandler(
registerGatewayMethod: ReturnType<typeof vi.fn>,
method: string,
):
| ((ctx: {
params: Record<string, unknown>;
respond: (ok: boolean, payload?: unknown, error?: unknown) => void;
}) => Promise<void>)
| undefined {
return registerGatewayMethod.mock.calls.find((call) => call[0] === method)?.[1];
}
describe("memory-wiki gateway methods", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(syncMemoryWikiImportedSources).mockResolvedValue({
importedCount: 0,
updatedCount: 0,
skippedCount: 0,
removedCount: 0,
artifactCount: 0,
workspaces: 0,
pagePaths: [],
indexesRefreshed: false,
indexUpdatedFiles: [],
indexRefreshReason: "no-import-changes",
});
vi.mocked(resolveMemoryWikiStatus).mockResolvedValue({
vaultMode: "isolated",
vaultExists: true,
} as never);
vi.mocked(ingestMemoryWikiSource).mockResolvedValue({
pagePath: "sources/alpha-notes.md",
} as never);
vi.mocked(listMemoryWikiImportRuns).mockResolvedValue({
runs: [],
totalRuns: 0,
activeRuns: 0,
rolledBackRuns: 0,
} as never);
vi.mocked(listMemoryWikiImportInsights).mockResolvedValue({
sourceType: "chatgpt",
totalItems: 0,
totalClusters: 0,
clusters: [],
} as never);
vi.mocked(listMemoryWikiPalace).mockResolvedValue({
totalItems: 0,
totalClaims: 0,
totalQuestions: 0,
totalContradictions: 0,
clusters: [],
} as never);
vi.mocked(normalizeMemoryWikiMutationInput).mockReturnValue({
op: "create_synthesis",
title: "Gateway Alpha",
body: "Gateway summary.",
sourceIds: ["source.alpha"],
} satisfies ApplyMemoryWikiMutation);
vi.mocked(applyMemoryWikiMutation).mockResolvedValue({
operation: "create_synthesis",
pagePath: "syntheses/gateway-alpha.md",
} as never);
vi.mocked(searchMemoryWiki).mockResolvedValue({
items: [],
total: 0,
} as never);
});
it("returns wiki status over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.status");
if (!handler) {
throw new Error("wiki.status handler missing");
}
const respond = vi.fn();
await handler({
params: {},
respond,
});
expect(syncMemoryWikiImportedSources).toHaveBeenCalledWith({ config, appConfig: undefined });
expect(resolveMemoryWikiStatus).toHaveBeenCalledWith(config, {
appConfig: undefined,
});
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
vaultMode: "isolated",
vaultExists: true,
}),
);
});
it("returns recent import runs over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
vi.mocked(listMemoryWikiImportRuns).mockResolvedValue({
runs: [
{
runId: "chatgpt-abc123",
importType: "chatgpt",
appliedAt: "2026-04-10T10:00:00.000Z",
exportPath: "/tmp/chatgpt",
sourcePath: "/tmp/chatgpt/conversations.json",
conversationCount: 12,
createdCount: 4,
updatedCount: 2,
skippedCount: 6,
status: "applied",
pagePaths: ["sources/chatgpt-2026-04-10-alpha.md"],
samplePaths: ["sources/chatgpt-2026-04-10-alpha.md"],
},
],
totalRuns: 1,
activeRuns: 1,
rolledBackRuns: 0,
} as never);
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.importRuns");
if (!handler) {
throw new Error("wiki.importRuns handler missing");
}
const respond = vi.fn();
await handler({
params: {
limit: 5,
},
respond,
});
expect(listMemoryWikiImportRuns).toHaveBeenCalledWith(config, { limit: 5 });
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
totalRuns: 1,
activeRuns: 1,
}),
);
});
it("returns import insights over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
vi.mocked(listMemoryWikiImportInsights).mockResolvedValue({
sourceType: "chatgpt",
totalItems: 2,
totalClusters: 1,
clusters: [
{
key: "topic/travel",
label: "Travel",
itemCount: 2,
highRiskCount: 1,
withheldCount: 1,
preferenceSignalCount: 0,
updatedAt: "2026-04-10T10:00:00.000Z",
items: [
{
pagePath: "sources/chatgpt-2026-04-10-alpha.md",
title: "BA flight receipts process",
riskLevel: "low",
labels: ["domain/personal", "area/travel", "topic/travel"],
topicKey: "topic/travel",
topicLabel: "Travel",
digestStatus: "available",
firstUserLine: "how do i get receipts?",
lastUserLine: "that option does not exist",
preferenceSignals: [],
},
],
},
],
} as never);
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.importInsights");
if (!handler) {
throw new Error("wiki.importInsights handler missing");
}
const respond = vi.fn();
await handler({
params: {},
respond,
});
expect(syncMemoryWikiImportedSources).toHaveBeenCalledWith({ config, appConfig: undefined });
expect(listMemoryWikiImportInsights).toHaveBeenCalledWith(config);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
sourceType: "chatgpt",
totalItems: 2,
totalClusters: 1,
}),
);
});
it("returns memory palace overview over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
vi.mocked(listMemoryWikiPalace).mockResolvedValue({
totalItems: 3,
totalClaims: 4,
totalQuestions: 1,
totalContradictions: 1,
clusters: [
{
key: "synthesis",
label: "Syntheses",
itemCount: 1,
claimCount: 2,
questionCount: 1,
contradictionCount: 0,
items: [
{
pagePath: "syntheses/travel-system.md",
title: "Travel system",
kind: "synthesis",
claimCount: 2,
questionCount: 1,
contradictionCount: 0,
claims: ["prefers direct receipts"],
questions: ["should this become a playbook?"],
contradictions: [],
},
],
},
],
} as never);
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.palace");
if (!handler) {
throw new Error("wiki.palace handler missing");
}
const respond = vi.fn();
await handler({
params: {},
respond,
});
expect(syncMemoryWikiImportedSources).toHaveBeenCalledWith({ config, appConfig: undefined });
expect(listMemoryWikiPalace).toHaveBeenCalledWith(config);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
totalItems: 3,
totalClaims: 4,
}),
);
});
it("validates required query params for wiki.search", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.search");
if (!handler) {
throw new Error("wiki.search handler missing");
}
const respond = vi.fn();
await handler({
params: {},
respond,
});
expect(searchMemoryWiki).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({ message: "query is required." }),
);
});
it("forwards wiki.search mode and corpus options over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.search");
if (!handler) {
throw new Error("wiki.search handler missing");
}
const respond = vi.fn();
await handler({
params: {
query: "Teams Azure",
maxResults: 3,
corpus: "wiki",
backend: "local",
mode: "route-question",
},
respond,
});
expect(searchMemoryWiki).toHaveBeenCalledWith(
expect.objectContaining({
config,
appConfig: undefined,
query: "Teams Azure",
maxResults: 3,
searchBackend: "local",
searchCorpus: "wiki",
mode: "route-question",
}),
);
expect(respond).toHaveBeenCalledWith(true, expect.anything());
});
it("forwards ingest requests over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.ingest");
if (!handler) {
throw new Error("wiki.ingest handler missing");
}
const respond = vi.fn();
await handler({
params: {
inputPath: "/tmp/alpha-notes.txt",
title: "Alpha",
},
respond,
});
expect(ingestMemoryWikiSource).toHaveBeenCalledWith({
config,
inputPath: "/tmp/alpha-notes.txt",
title: "Alpha",
});
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
pagePath: "sources/alpha-notes.md",
}),
);
});
it("applies wiki mutations over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.apply");
if (!handler) {
throw new Error("wiki.apply handler missing");
}
const respond = vi.fn();
const params = {
op: "create_synthesis",
title: "Gateway Alpha",
body: "Gateway summary.",
sourceIds: ["source.alpha"],
};
await handler({
params,
respond,
});
expect(normalizeMemoryWikiMutationInput).toHaveBeenCalledWith(params);
expect(applyMemoryWikiMutation).toHaveBeenCalledWith({
config,
mutation: expect.objectContaining({
op: "create_synthesis",
title: "Gateway Alpha",
}),
});
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
operation: "create_synthesis",
pagePath: "syntheses/gateway-alpha.md",
}),
);
});
});