mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 01:40:23 +00:00
test(integration): dedupe messaging, secrets, and plugin test suites
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user