test(integration): dedupe messaging, secrets, and plugin test suites

This commit is contained in:
Peter Steinberger
2026-03-02 06:41:31 +00:00
parent d3e0c0b29c
commit 45888276a3
21 changed files with 1840 additions and 2416 deletions

View File

@@ -127,6 +127,17 @@ describe("memory index", () => {
};
}
function requireManager(
result: Awaited<ReturnType<typeof getMemorySearchManager>>,
missingMessage = "manager missing",
): MemoryIndexManager {
expect(result.manager).not.toBeNull();
if (!result.manager) {
throw new Error(missingMessage);
}
return result.manager as MemoryIndexManager;
}
async function getPersistentManager(cfg: TestCfg): Promise<MemoryIndexManager> {
const storePath = cfg.agents?.defaults?.memorySearch?.store?.path;
if (!storePath) {
@@ -139,17 +150,26 @@ describe("memory index", () => {
}
const result = await getMemorySearchManager({ cfg, agentId: "main" });
expect(result.manager).not.toBeNull();
if (!result.manager) {
throw new Error("manager missing");
}
const manager = result.manager as MemoryIndexManager;
const manager = requireManager(result);
managersByStorePath.set(storePath, manager);
managersForCleanup.add(manager);
resetManagerForTest(manager);
return manager;
}
async function expectHybridKeywordSearchFindsMemory(cfg: TestCfg) {
const manager = await getPersistentManager(cfg);
const status = manager.status();
if (!status.fts?.available) {
return;
}
await manager.sync({ reason: "test" });
const results = await manager.search("zebra");
expect(results.length).toBeGreaterThan(0);
expect(results[0]?.path).toContain("memory/2026-01-12.md");
}
it("indexes memory files and searches", async () => {
const cfg = createCfg({
storePath: indexMainPath,
@@ -178,26 +198,19 @@ describe("memory index", () => {
const cfg = createCfg({ storePath: indexStatusPath });
const first = await getMemorySearchManager({ cfg, agentId: "main" });
expect(first.manager).not.toBeNull();
if (!first.manager) {
throw new Error("manager missing");
}
await first.manager.sync?.({ reason: "test" });
await first.manager.close?.();
const firstManager = requireManager(first);
await firstManager.sync?.({ reason: "test" });
await firstManager.close?.();
const statusOnly = await getMemorySearchManager({
cfg,
agentId: "main",
purpose: "status",
});
expect(statusOnly.manager).not.toBeNull();
if (!statusOnly.manager) {
throw new Error("status manager missing");
}
const status = statusOnly.manager.status();
const statusManager = requireManager(statusOnly, "status manager missing");
const status = statusManager.status();
expect(status.dirty).toBe(false);
await statusOnly.manager.close?.();
await statusManager.close?.();
});
it("reindexes sessions when source config adds sessions to an existing index", async () => {
@@ -244,31 +257,25 @@ describe("memory index", () => {
try {
const first = await getMemorySearchManager({ cfg: firstCfg, agentId: "main" });
expect(first.manager).not.toBeNull();
if (!first.manager) {
throw new Error("manager missing");
}
await first.manager.sync?.({ reason: "test" });
const firstStatus = first.manager.status();
const firstManager = requireManager(first);
await firstManager.sync?.({ reason: "test" });
const firstStatus = firstManager.status();
expect(
firstStatus.sourceCounts?.find((entry) => entry.source === "sessions")?.files ?? 0,
).toBe(0);
await first.manager.close?.();
await firstManager.close?.();
const second = await getMemorySearchManager({ cfg: secondCfg, agentId: "main" });
expect(second.manager).not.toBeNull();
if (!second.manager) {
throw new Error("manager missing");
}
await second.manager.sync?.({ reason: "test" });
const secondStatus = second.manager.status();
const secondManager = requireManager(second);
await secondManager.sync?.({ reason: "test" });
const secondStatus = secondManager.status();
expect(secondStatus.sourceCounts?.find((entry) => entry.source === "sessions")?.files).toBe(
1,
);
expect(
secondStatus.sourceCounts?.find((entry) => entry.source === "sessions")?.chunks ?? 0,
).toBeGreaterThan(0);
await second.manager.close?.();
await secondManager.close?.();
} finally {
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
@@ -302,13 +309,10 @@ describe("memory index", () => {
},
agentId: "main",
});
expect(first.manager).not.toBeNull();
if (!first.manager) {
throw new Error("manager missing");
}
await first.manager.sync?.({ reason: "test" });
const firstManager = requireManager(first);
await firstManager.sync?.({ reason: "test" });
const callsAfterFirstSync = embedBatchCalls;
await first.manager.close?.();
await firstManager.close?.();
const second = await getMemorySearchManager({
cfg: {
@@ -326,15 +330,12 @@ describe("memory index", () => {
},
agentId: "main",
});
expect(second.manager).not.toBeNull();
if (!second.manager) {
throw new Error("manager missing");
}
await second.manager.sync?.({ reason: "test" });
const secondManager = requireManager(second);
await secondManager.sync?.({ reason: "test" });
expect(embedBatchCalls).toBeGreaterThan(callsAfterFirstSync);
const status = second.manager.status();
const status = secondManager.status();
expect(status.files).toBeGreaterThan(0);
await second.manager.close?.();
await secondManager.close?.();
});
it("reuses cached embeddings on forced reindex", async () => {
@@ -351,40 +352,22 @@ describe("memory index", () => {
});
it("finds keyword matches via hybrid search when query embedding is zero", async () => {
const cfg = createCfg({
storePath: indexMainPath,
hybrid: { enabled: true, vectorWeight: 0, textWeight: 1 },
});
const manager = await getPersistentManager(cfg);
const status = manager.status();
if (!status.fts?.available) {
return;
}
await manager.sync({ reason: "test" });
const results = await manager.search("zebra");
expect(results.length).toBeGreaterThan(0);
expect(results[0]?.path).toContain("memory/2026-01-12.md");
await expectHybridKeywordSearchFindsMemory(
createCfg({
storePath: indexMainPath,
hybrid: { enabled: true, vectorWeight: 0, textWeight: 1 },
}),
);
});
it("preserves keyword-only hybrid hits when minScore exceeds text weight", async () => {
const cfg = createCfg({
storePath: indexMainPath,
minScore: 0.35,
hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
});
const manager = await getPersistentManager(cfg);
const status = manager.status();
if (!status.fts?.available) {
return;
}
await manager.sync({ reason: "test" });
const results = await manager.search("zebra");
expect(results.length).toBeGreaterThan(0);
expect(results[0]?.path).toContain("memory/2026-01-12.md");
await expectHybridKeywordSearchFindsMemory(
createCfg({
storePath: indexMainPath,
minScore: 0.35,
hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
}),
);
});
it("reports vector availability after probe", async () => {

View File

@@ -13,6 +13,51 @@ describe("memory manager readonly recovery", () => {
let indexPath = "";
let manager: MemoryIndexManager | null = null;
function createMemoryConfig(): OpenClawConfig {
return {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: false },
},
},
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
}
async function createManager() {
manager = await getRequiredMemoryIndexManager({ cfg: createMemoryConfig(), agentId: "main" });
return manager;
}
function createSyncSpies(instance: MemoryIndexManager) {
const runSyncSpy = vi.spyOn(
instance as unknown as {
runSync: (params?: { reason?: string; force?: boolean }) => Promise<void>;
},
"runSync",
);
const openDatabaseSpy = vi.spyOn(
instance as unknown as { openDatabase: () => DatabaseSync },
"openDatabase",
);
return { runSyncSpy, openDatabaseSpy };
}
function expectReadonlyRecoveryStatus(lastError: string) {
expect(manager?.status().custom?.readonlyRecovery).toEqual({
attempts: 1,
successes: 1,
failures: 0,
lastError,
});
}
beforeEach(async () => {
resetEmbeddingMocks();
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-readonly-"));
@@ -30,124 +75,39 @@ describe("memory manager readonly recovery", () => {
});
it("reopens sqlite and retries once when sync hits SQLITE_READONLY", async () => {
const cfg = {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: false },
},
},
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
manager = await getRequiredMemoryIndexManager({ cfg, agentId: "main" });
const runSyncSpy = vi.spyOn(
manager as unknown as {
runSync: (params?: { reason?: string; force?: boolean }) => Promise<void>;
},
"runSync",
);
const currentManager = await createManager();
const { runSyncSpy, openDatabaseSpy } = createSyncSpies(currentManager);
runSyncSpy
.mockRejectedValueOnce(new Error("attempt to write a readonly database"))
.mockResolvedValueOnce(undefined);
const openDatabaseSpy = vi.spyOn(
manager as unknown as { openDatabase: () => DatabaseSync },
"openDatabase",
);
await manager.sync({ reason: "test" });
await currentManager.sync({ reason: "test" });
expect(runSyncSpy).toHaveBeenCalledTimes(2);
expect(openDatabaseSpy).toHaveBeenCalledTimes(1);
expect(manager.status().custom?.readonlyRecovery).toEqual({
attempts: 1,
successes: 1,
failures: 0,
lastError: "attempt to write a readonly database",
});
expectReadonlyRecoveryStatus("attempt to write a readonly database");
});
it("reopens sqlite and retries when readonly appears in error code", async () => {
const cfg = {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: false },
},
},
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
manager = await getRequiredMemoryIndexManager({ cfg, agentId: "main" });
const runSyncSpy = vi.spyOn(
manager as unknown as {
runSync: (params?: { reason?: string; force?: boolean }) => Promise<void>;
},
"runSync",
);
const currentManager = await createManager();
const { runSyncSpy, openDatabaseSpy } = createSyncSpies(currentManager);
runSyncSpy
.mockRejectedValueOnce({ message: "write failed", code: "SQLITE_READONLY" })
.mockResolvedValueOnce(undefined);
const openDatabaseSpy = vi.spyOn(
manager as unknown as { openDatabase: () => DatabaseSync },
"openDatabase",
);
await manager.sync({ reason: "test" });
await currentManager.sync({ reason: "test" });
expect(runSyncSpy).toHaveBeenCalledTimes(2);
expect(openDatabaseSpy).toHaveBeenCalledTimes(1);
expect(manager.status().custom?.readonlyRecovery).toEqual({
attempts: 1,
successes: 1,
failures: 0,
lastError: "write failed",
});
expectReadonlyRecoveryStatus("write failed");
});
it("does not retry non-readonly sync errors", async () => {
const cfg = {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: false },
},
},
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
manager = await getRequiredMemoryIndexManager({ cfg, agentId: "main" });
const runSyncSpy = vi.spyOn(
manager as unknown as {
runSync: (params?: { reason?: string; force?: boolean }) => Promise<void>;
},
"runSync",
);
const currentManager = await createManager();
const { runSyncSpy, openDatabaseSpy } = createSyncSpies(currentManager);
runSyncSpy.mockRejectedValueOnce(new Error("embedding timeout"));
const openDatabaseSpy = vi.spyOn(
manager as unknown as { openDatabase: () => DatabaseSync },
"openDatabase",
);
await expect(manager.sync({ reason: "test" })).rejects.toThrow("embedding timeout");
await expect(currentManager.sync({ reason: "test" })).rejects.toThrow("embedding timeout");
expect(runSyncSpy).toHaveBeenCalledTimes(1);
expect(openDatabaseSpy).toHaveBeenCalledTimes(0);
});