From 577438ca73a20e6528377ec59ec38578741e6a68 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 13:13:12 +0100 Subject: [PATCH] test: dedupe mirrored memory and deepseek tests --- extensions/deepseek/index.test.ts | 192 ++---- .../host/backend-config.test.ts | 548 +----------------- .../host/embeddings-remote-fetch.test.ts | 60 +- src/memory-host-sdk/host/post-json.test.ts | 70 +-- 4 files changed, 64 insertions(+), 806 deletions(-) diff --git a/extensions/deepseek/index.test.ts b/extensions/deepseek/index.test.ts index 6f659336e54..b0b014a3378 100644 --- a/extensions/deepseek/index.test.ts +++ b/extensions/deepseek/index.test.ts @@ -10,6 +10,48 @@ import { runSingleProviderCatalog } from "../test-support/provider-model-test-he import deepseekPlugin from "./index.js"; import { createDeepSeekV4ThinkingWrapper } from "./stream.js"; +type OpenAICompletionsModel = Model<"openai-completions">; + +type PayloadCapture = { + payload?: Record; +}; + +function deepSeekV4Model(id: "deepseek-v4-flash" | "deepseek-v4-pro"): OpenAICompletionsModel { + return { + provider: "deepseek", + id, + name: id === "deepseek-v4-flash" ? "DeepSeek V4 Flash" : "DeepSeek V4 Pro", + api: "openai-completions", + baseUrl: "https://api.deepseek.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 384_000, + compat: { + supportsUsageInStreaming: true, + supportsReasoningEffort: true, + maxTokensField: "max_tokens", + }, + } as OpenAICompletionsModel; +} + +function createPayloadCapturingStream(capture: PayloadCapture) { + return ( + streamModel: OpenAICompletionsModel, + streamContext: Context, + options?: { onPayload?: (payload: unknown, model: unknown) => unknown }, + ) => { + capture.payload = buildOpenAICompletionsParams(streamModel, streamContext, { + reasoning: "high", + } as never); + options?.onPayload?.(capture.payload, streamModel); + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => stream.end()); + return stream; + }; +} + describe("deepseek provider plugin", () => { it("registers DeepSeek with api-key auth wizard metadata", async () => { const provider = await registerSingleProviderPlugin(deepseekPlugin); @@ -119,24 +161,8 @@ describe("deepseek provider plugin", () => { }); it("preserves replayed reasoning_content when DeepSeek V4 thinking is enabled", async () => { - let capturedPayload: Record | undefined; - const model = { - provider: "deepseek", - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - baseUrl: "https://api.deepseek.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 384_000, - compat: { - supportsUsageInStreaming: true, - supportsReasoningEffort: true, - maxTokensField: "max_tokens", - }, - } as Model<"openai-completions">; + const capture: PayloadCapture = {}; + const model = deepSeekV4Model("deepseek-v4-flash"); const context = { messages: [ { role: "user", content: "hi", timestamp: 1 }, @@ -181,29 +207,17 @@ describe("deepseek provider plugin", () => { }, ], } as Context; - const baseStreamFn = ( - streamModel: Model<"openai-completions">, - streamContext: Context, - options?: { onPayload?: (payload: unknown, model: unknown) => unknown }, - ) => { - capturedPayload = buildOpenAICompletionsParams(streamModel, streamContext, { - reasoning: "high", - } as never); - options?.onPayload?.(capturedPayload, streamModel); - const stream = createAssistantMessageEventStream(); - queueMicrotask(() => stream.end()); - return stream; - }; + const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"); expect(wrapThinkingHigh).toBeDefined(); await wrapThinkingHigh?.(model, context, {}); - expect(capturedPayload).toMatchObject({ + expect(capture.payload).toMatchObject({ thinking: { type: "enabled" }, reasoning_effort: "high", }); - expect((capturedPayload?.messages as Array>)[1]).toMatchObject({ + expect((capture.payload?.messages as Array>)[1]).toMatchObject({ role: "assistant", reasoning_content: "call reasoning", tool_calls: [ @@ -220,24 +234,8 @@ describe("deepseek provider plugin", () => { }); it("adds blank reasoning_content for replayed tool calls from non-DeepSeek turns", async () => { - let capturedPayload: Record | undefined; - const model = { - provider: "deepseek", - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - baseUrl: "https://api.deepseek.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 384_000, - compat: { - supportsUsageInStreaming: true, - supportsReasoningEffort: true, - maxTokensField: "max_tokens", - }, - } as Model<"openai-completions">; + const capture: PayloadCapture = {}; + const model = deepSeekV4Model("deepseek-v4-pro"); const context = { messages: [ { role: "user", content: "hi", timestamp: 1 }, @@ -275,25 +273,13 @@ describe("deepseek provider plugin", () => { }, ], } as Context; - const baseStreamFn = ( - streamModel: Model<"openai-completions">, - streamContext: Context, - options?: { onPayload?: (payload: unknown, model: unknown) => unknown }, - ) => { - capturedPayload = buildOpenAICompletionsParams(streamModel, streamContext, { - reasoning: "high", - } as never); - options?.onPayload?.(capturedPayload, streamModel); - const stream = createAssistantMessageEventStream(); - queueMicrotask(() => stream.end()); - return stream; - }; + const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"); expect(wrapThinkingHigh).toBeDefined(); await wrapThinkingHigh?.(model, context, {}); - expect((capturedPayload?.messages as Array>)[1]).toMatchObject({ + expect((capture.payload?.messages as Array>)[1]).toMatchObject({ role: "assistant", reasoning_content: "", tool_calls: [ @@ -310,24 +296,8 @@ describe("deepseek provider plugin", () => { }); it("adds blank reasoning_content for replayed plain assistant messages", async () => { - let capturedPayload: Record | undefined; - const model = { - provider: "deepseek", - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - baseUrl: "https://api.deepseek.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 384_000, - compat: { - supportsUsageInStreaming: true, - supportsReasoningEffort: true, - maxTokensField: "max_tokens", - }, - } as Model<"openai-completions">; + const capture: PayloadCapture = {}; + const model = deepSeekV4Model("deepseek-v4-pro"); const context = { messages: [ { role: "user", content: "hi", timestamp: 1 }, @@ -351,25 +321,13 @@ describe("deepseek provider plugin", () => { { role: "user", content: "next", timestamp: 3 }, ], } as Context; - const baseStreamFn = ( - streamModel: Model<"openai-completions">, - streamContext: Context, - options?: { onPayload?: (payload: unknown, model: unknown) => unknown }, - ) => { - capturedPayload = buildOpenAICompletionsParams(streamModel, streamContext, { - reasoning: "high", - } as never); - options?.onPayload?.(capturedPayload, streamModel); - const stream = createAssistantMessageEventStream(); - queueMicrotask(() => stream.end()); - return stream; - }; + const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"); expect(wrapThinkingHigh).toBeDefined(); await wrapThinkingHigh?.(model, context, {}); - expect((capturedPayload?.messages as Array>)[1]).toMatchObject({ + expect((capture.payload?.messages as Array>)[1]).toMatchObject({ role: "assistant", content: "Hello.", reasoning_content: "", @@ -377,24 +335,8 @@ describe("deepseek provider plugin", () => { }); it("strips replayed reasoning_content when DeepSeek V4 thinking is disabled", async () => { - let capturedPayload: Record | undefined; - const model = { - provider: "deepseek", - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - baseUrl: "https://api.deepseek.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 384_000, - compat: { - supportsUsageInStreaming: true, - supportsReasoningEffort: true, - maxTokensField: "max_tokens", - }, - } as Model<"openai-completions">; + const capture: PayloadCapture = {}; + const model = deepSeekV4Model("deepseek-v4-flash"); const context = { messages: [ { role: "user", content: "hi", timestamp: 1 }, @@ -439,19 +381,7 @@ describe("deepseek provider plugin", () => { }, ], } as Context; - const baseStreamFn = ( - streamModel: Model<"openai-completions">, - streamContext: Context, - options?: { onPayload?: (payload: unknown, model: unknown) => unknown }, - ) => { - capturedPayload = buildOpenAICompletionsParams(streamModel, streamContext, { - reasoning: "high", - } as never); - options?.onPayload?.(capturedPayload, streamModel); - const stream = createAssistantMessageEventStream(); - queueMicrotask(() => stream.end()); - return stream; - }; + const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingNone = createDeepSeekV4ThinkingWrapper( baseStreamFn as never, @@ -460,9 +390,9 @@ describe("deepseek provider plugin", () => { expect(wrapThinkingNone).toBeDefined(); await wrapThinkingNone?.(model, context, {}); - expect(capturedPayload).toMatchObject({ thinking: { type: "disabled" } }); - expect(capturedPayload).not.toHaveProperty("reasoning_effort"); - expect((capturedPayload?.messages as Array>)[1]).not.toHaveProperty( + expect(capture.payload).toMatchObject({ thinking: { type: "disabled" } }); + expect(capture.payload).not.toHaveProperty("reasoning_effort"); + expect((capture.payload?.messages as Array>)[1]).not.toHaveProperty( "reasoning_content", ); }); diff --git a/src/memory-host-sdk/host/backend-config.test.ts b/src/memory-host-sdk/host/backend-config.test.ts index cfbe14cf9ff..d0abfb71d68 100644 --- a/src/memory-host-sdk/host/backend-config.test.ts +++ b/src/memory-host-sdk/host/backend-config.test.ts @@ -1,547 +1 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { resolveAgentWorkspaceDir } from "../../agents/agent-scope-config.js"; -import type { OpenClawConfig } from "../../config/config.js"; -import { resolveMemoryBackendConfig } from "./backend-config.js"; -import { isQmdScopeAllowed } from "./qmd-scope.js"; - -type QmdPathFixture = { - path: string; - name?: string; - pattern?: string; -}; - -const resolveComparablePath = (value: string, workspaceDir = "/workspace/root"): string => - path.isAbsolute(value) ? path.resolve(value) : path.resolve(workspaceDir, value); - -function resolveCollectionNamesForAgent(cfg: OpenClawConfig, agentId: string): Set { - return new Set( - (resolveMemoryBackendConfig({ cfg, agentId }).qmd?.collections ?? []).map( - (collection) => collection.name, - ), - ); -} - -function resolveCustomCollectionPathsForAgent(cfg: OpenClawConfig, agentId: string): string[] { - return (resolveMemoryBackendConfig({ cfg, agentId }).qmd?.collections ?? []) - .filter((collection) => collection.kind === "custom") - .map((collection) => collection.path); -} - -function qmdMultiAgentConfig(paths: QmdPathFixture[]) { - return { - agents: { - defaults: { workspace: "/workspace/root" }, - list: [ - { id: "main", default: true, workspace: "/workspace/root" }, - { id: "dev", workspace: "/workspace/dev" }, - ], - }, - memory: { - backend: "qmd", - qmd: { - includeDefaultMemory: true, - paths, - }, - }, - } as OpenClawConfig; -} - -describe("resolveMemoryBackendConfig", () => { - it("defaults to builtin backend when config missing", () => { - const cfg = { agents: { defaults: { workspace: "/tmp/memory-test" } } } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.backend).toBe("builtin"); - expect(resolved.citations).toBe("auto"); - expect(resolved.qmd).toBeUndefined(); - }); - - it("resolves qmd backend with default collections", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: {}, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.backend).toBe("qmd"); - expect(resolved.qmd?.collections.length).toBe(2); - expect(resolved.qmd?.command).toBe("qmd"); - expect(resolved.qmd?.searchMode).toBe("search"); - expect(resolved.qmd?.update.intervalMs).toBeGreaterThan(0); - expect(resolved.qmd?.update.onBoot).toBe(true); - expect(resolved.qmd?.update.startup).toBe("off"); - expect(resolved.qmd?.update.startupDelayMs).toBe(120_000); - expect(resolved.qmd?.update.waitForBootSync).toBe(false); - expect(resolved.qmd?.update.commandTimeoutMs).toBe(30_000); - expect(resolved.qmd?.update.updateTimeoutMs).toBe(120_000); - expect(resolved.qmd?.update.embedTimeoutMs).toBe(120_000); - const names = new Set((resolved.qmd?.collections ?? []).map((collection) => collection.name)); - expect(names.has("memory-root-main")).toBe(true); - expect(names.has("memory-dir-main")).toBe(true); - }); - - it("allows direct and channel sessions in the default qmd scope", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: {}, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - - expect(isQmdScopeAllowed(resolved.qmd?.scope, "agent:main:discord:direct:user-123")).toBe(true); - expect(isQmdScopeAllowed(resolved.qmd?.scope, "agent:main:discord:channel:chan-123")).toBe( - true, - ); - expect(isQmdScopeAllowed(resolved.qmd?.scope, "agent:main:discord:group:group-123")).toBe( - false, - ); - }); - - it("parses quoted qmd command paths", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - command: '"/Applications/QMD Tools/qmd" --flag', - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.command).toBe("/Applications/QMD Tools/qmd"); - }); - - it("preserves explicit homebrew qmd paths for service environments", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - command: "/opt/homebrew/bin/qmd", - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.command).toBe("/opt/homebrew/bin/qmd"); - }); - - it("resolves custom paths relative to workspace", () => { - const cfg = { - agents: { - defaults: { workspace: "/workspace/root" }, - list: [{ id: "main", workspace: "/workspace/root" }], - }, - memory: { - backend: "qmd", - qmd: { - paths: [ - { - path: "notes", - name: "custom-notes", - pattern: "**/*.md", - }, - ], - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - const custom = resolved.qmd?.collections.find((c) => c.name.startsWith("custom-notes")); - expect(custom).toBeDefined(); - const workspaceRoot = resolveAgentWorkspaceDir(cfg, "main"); - expect(custom?.path).toBe(path.resolve(workspaceRoot, "notes")); - }); - - it("scopes qmd collection names per agent", () => { - const cfg = qmdMultiAgentConfig([{ path: "notes", name: "workspace", pattern: "**/*.md" }]); - const mainNames = resolveCollectionNamesForAgent(cfg, "main"); - const devNames = resolveCollectionNamesForAgent(cfg, "dev"); - expect(mainNames.has("memory-dir-main")).toBe(true); - expect(devNames.has("memory-dir-dev")).toBe(true); - expect(mainNames.has("workspace-main")).toBe(true); - expect(devNames.has("workspace-dev")).toBe(true); - }); - - it("merges default and per-agent qmd extra collections", () => { - const cfg = { - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - qmd: { - extraCollections: [ - { - path: "/shared/team-notes", - name: "team-notes", - pattern: "**/*.md", - }, - ], - }, - }, - }, - list: [ - { - id: "main", - default: true, - workspace: "/workspace/root", - memorySearch: { - qmd: { - extraCollections: [ - { - path: "notes", - name: "notes", - pattern: "**/*.md", - }, - ], - }, - }, - }, - ], - }, - memory: { - backend: "qmd", - qmd: { - includeDefaultMemory: false, - }, - }, - } as OpenClawConfig; - const names = resolveCollectionNamesForAgent(cfg, "main"); - expect(names.has("team-notes")).toBe(true); - expect(names.has("notes-main")).toBe(true); - }); - - it("preserves explicit custom collection names for paths outside the workspace", () => { - const cfg = qmdMultiAgentConfig([ - { path: "/shared/notion-mirror", name: "notion-mirror", pattern: "**/*.md" }, - ]); - const mainNames = resolveCollectionNamesForAgent(cfg, "main"); - const devNames = resolveCollectionNamesForAgent(cfg, "dev"); - expect(mainNames.has("memory-dir-main")).toBe(true); - expect(devNames.has("memory-dir-dev")).toBe(true); - expect(mainNames.has("notion-mirror")).toBe(true); - expect(devNames.has("notion-mirror")).toBe(true); - }); - - it("keeps symlinked workspace paths agent-scoped when deciding custom collection names", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-backend-config-")); - const workspaceDir = path.join(tmpRoot, "workspace"); - const workspaceAliasDir = path.join(tmpRoot, "workspace-alias"); - try { - await fs.mkdir(workspaceDir, { recursive: true }); - await fs.symlink(workspaceDir, workspaceAliasDir); - const cfg = { - agents: { - defaults: { workspace: workspaceDir }, - list: [{ id: "main", default: true, workspace: workspaceDir }], - }, - memory: { - backend: "qmd", - qmd: { - includeDefaultMemory: false, - paths: [{ path: workspaceAliasDir, name: "workspace", pattern: "**/*.md" }], - }, - }, - } as OpenClawConfig; - const names = resolveCollectionNamesForAgent(cfg, "main"); - expect(names.has("workspace-main")).toBe(true); - expect(names.has("workspace")).toBe(false); - } finally { - await fs.rm(tmpRoot, { recursive: true, force: true }); - } - }); - - it("keeps unresolved child paths under a symlinked workspace agent-scoped", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-backend-config-")); - const realRootDir = path.join(tmpRoot, "real-root"); - const aliasRootDir = path.join(tmpRoot, "alias-root"); - const workspaceDir = path.join(realRootDir, "workspace"); - const workspaceAliasDir = path.join(aliasRootDir, "workspace"); - try { - await fs.mkdir(workspaceDir, { recursive: true }); - await fs.symlink(realRootDir, aliasRootDir); - const cfg = { - agents: { - defaults: { workspace: workspaceDir }, - list: [{ id: "main", default: true, workspace: workspaceDir }], - }, - memory: { - backend: "qmd", - qmd: { - includeDefaultMemory: false, - paths: [ - { path: path.join(workspaceAliasDir, "notes"), name: "notes", pattern: "**/*.md" }, - ], - }, - }, - } as OpenClawConfig; - const names = resolveCollectionNamesForAgent(cfg, "main"); - expect(names.has("notes-main")).toBe(true); - expect(names.has("notes")).toBe(false); - } finally { - await fs.rm(tmpRoot, { recursive: true, force: true }); - } - }); - - it("resolves qmd update timeout overrides", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - update: { - waitForBootSync: true, - commandTimeoutMs: 12_000, - updateTimeoutMs: 480_000, - embedTimeoutMs: 360_000, - }, - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.update.waitForBootSync).toBe(true); - expect(resolved.qmd?.update.commandTimeoutMs).toBe(12_000); - expect(resolved.qmd?.update.updateTimeoutMs).toBe(480_000); - expect(resolved.qmd?.update.embedTimeoutMs).toBe(360_000); - }); - - it("resolves qmd startup refresh overrides", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - update: { - startup: "idle", - startupDelayMs: 45_000, - }, - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.update.startup).toBe("idle"); - expect(resolved.qmd?.update.startupDelayMs).toBe(45_000); - expect(resolved.qmd?.update.onBoot).toBe(true); - }); - - it("resolves qmd search mode override", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - searchMode: "vsearch", - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.searchMode).toBe("vsearch"); - }); - - it("resolves qmd mcporter search tool override", () => { - const cfg = { - agents: { defaults: { workspace: "/tmp/memory-test" } }, - memory: { - backend: "qmd", - qmd: { - searchMode: "query", - searchTool: " hybrid_search ", - }, - }, - } as OpenClawConfig; - const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - expect(resolved.qmd?.searchMode).toBe("query"); - expect(resolved.qmd?.searchTool).toBe("hybrid_search"); - }); -}); - -describe("memorySearch.extraPaths integration", () => { - it("maps agents.defaults.memorySearch.extraPaths to QMD collections", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["/home/user/docs", "/home/user/vault"], - }, - }, - }, - } as OpenClawConfig; - const result = resolveMemoryBackendConfig({ cfg, agentId: "test-agent" }); - expect(result.backend).toBe("qmd"); - const customCollections = (result.qmd?.collections ?? []).filter( - (collection) => collection.kind === "custom", - ); - expect(customCollections.length).toBeGreaterThanOrEqual(2); - expect(customCollections.map((collection) => collection.path)).toEqual( - expect.arrayContaining([ - resolveComparablePath("/home/user/docs"), - resolveComparablePath("/home/user/vault"), - ]), - ); - }); - - it("merges default and per-agent memorySearch.extraPaths for QMD collections", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["/default/path"], - }, - }, - list: [ - { - id: "my-agent", - memorySearch: { - extraPaths: ["/agent/specific/path"], - }, - }, - ], - }, - } as OpenClawConfig; - const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" }); - expect(result.backend).toBe("qmd"); - const paths = resolveCustomCollectionPathsForAgent(cfg, "my-agent"); - expect(paths).toContain(resolveComparablePath("/agent/specific/path")); - expect(paths).toContain(resolveComparablePath("/default/path")); - }); - - it("falls back to defaults when agent has no overrides", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["/default/path"], - }, - }, - list: [ - { - id: "other-agent", - memorySearch: { - extraPaths: ["/other/path"], - }, - }, - ], - }, - } as OpenClawConfig; - const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" }); - expect(result.backend).toBe("qmd"); - const paths = resolveCustomCollectionPathsForAgent(cfg, "my-agent"); - expect(paths).toContain(resolveComparablePath("/default/path")); - }); - - it("deduplicates merged memorySearch.extraPaths for QMD collections", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["/shared/path", " /shared/path "], - }, - }, - list: [ - { - id: "my-agent", - memorySearch: { - extraPaths: ["/shared/path", "/agent-only"], - }, - }, - ], - }, - } as OpenClawConfig; - - const paths = resolveCustomCollectionPathsForAgent(cfg, "my-agent"); - - expect( - paths.filter((collectionPath) => collectionPath === resolveComparablePath("/shared/path")), - ).toHaveLength(1); - expect(paths).toContain(resolveComparablePath("/agent-only")); - }); - - it("keeps unnamed extra paths agent-scoped even when they resolve outside the workspace", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["/shared/path"], - }, - }, - }, - } as OpenClawConfig; - const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" }); - const customCollections = (result.qmd?.collections ?? []).filter( - (collection) => collection.kind === "custom", - ); - expect(customCollections.map((collection) => collection.name)).toContain("custom-1-my-agent"); - }); - - it("matches per-agent memorySearch.extraPaths using normalized agent ids", () => { - const cfg = { - memory: { backend: "qmd" }, - agents: { - defaults: { - workspace: "/workspace/root", - }, - list: [ - { - id: "My-Agent", - memorySearch: { - extraPaths: ["/agent/mixed-case"], - }, - }, - ], - }, - } as OpenClawConfig; - - const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" }); - const customCollections = (result.qmd?.collections ?? []).filter( - (collection) => collection.kind === "custom", - ); - - expect(customCollections.map((collection) => collection.path)).toContain( - resolveComparablePath("/agent/mixed-case"), - ); - }); - - it("deduplicates identical roots shared by memory.qmd.paths and memorySearch.extraPaths", () => { - const cfg = { - memory: { - backend: "qmd", - qmd: { - paths: [{ path: "docs", pattern: "**/*.md", name: "workspace-docs" }], - }, - }, - agents: { - defaults: { - workspace: "/workspace/root", - memorySearch: { - extraPaths: ["./docs"], - }, - }, - }, - } as OpenClawConfig; - - const result = resolveMemoryBackendConfig({ cfg, agentId: "main" }); - const customCollections = (result.qmd?.collections ?? []).filter( - (collection) => collection.kind === "custom", - ); - const docsCollections = customCollections.filter( - (collection) => - collection.path === resolveComparablePath("./docs") && collection.pattern === "**/*.md", - ); - - expect(docsCollections).toHaveLength(1); - }); -}); +import "../../../packages/memory-host-sdk/src/host/backend-config.test.js"; diff --git a/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts b/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts index 3ddddc708f5..e8e412994c7 100644 --- a/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts +++ b/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts @@ -1,59 +1 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -const postJsonMock = vi.hoisted(() => vi.fn()); - -vi.mock("./post-json.js", () => ({ - postJson: postJsonMock, -})); - -type EmbeddingsRemoteFetchModule = typeof import("./embeddings-remote-fetch.js"); - -let fetchRemoteEmbeddingVectors: EmbeddingsRemoteFetchModule["fetchRemoteEmbeddingVectors"]; - -describe("fetchRemoteEmbeddingVectors", () => { - beforeAll(async () => { - ({ fetchRemoteEmbeddingVectors } = await import("./embeddings-remote-fetch.js")); - }); - - beforeEach(() => { - postJsonMock.mockReset(); - }); - - it("maps remote embedding response data to vectors", async () => { - postJsonMock.mockImplementationOnce(async (params) => { - return await params.parse({ - data: [{ embedding: [0.1, 0.2] }, {}, { embedding: [0.3] }], - }); - }); - - const vectors = await fetchRemoteEmbeddingVectors({ - url: "https://memory.example/v1/embeddings", - headers: { Authorization: "Bearer test" }, - body: { input: ["one", "two", "three"] }, - errorPrefix: "embedding fetch failed", - }); - - expect(vectors).toEqual([[0.1, 0.2], [], [0.3]]); - expect(postJsonMock).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://memory.example/v1/embeddings", - headers: { Authorization: "Bearer test" }, - body: { input: ["one", "two", "three"] }, - errorPrefix: "embedding fetch failed", - }), - ); - }); - - it("throws a status-rich error on non-ok responses", async () => { - postJsonMock.mockRejectedValueOnce(new Error("embedding fetch failed: 403 forbidden")); - - await expect( - fetchRemoteEmbeddingVectors({ - url: "https://memory.example/v1/embeddings", - headers: {}, - body: { input: ["one"] }, - errorPrefix: "embedding fetch failed", - }), - ).rejects.toThrow("embedding fetch failed: 403 forbidden"); - }); -}); +import "../../../packages/memory-host-sdk/src/host/embeddings-remote-fetch.test.js"; diff --git a/src/memory-host-sdk/host/post-json.test.ts b/src/memory-host-sdk/host/post-json.test.ts index fb8847b8184..0ecaafff39e 100644 --- a/src/memory-host-sdk/host/post-json.test.ts +++ b/src/memory-host-sdk/host/post-json.test.ts @@ -1,69 +1 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("./remote-http.js", () => ({ - withRemoteHttpResponse: vi.fn(), -})); - -const { postJson } = await import("./post-json.js"); -const { withRemoteHttpResponse } = await import("./remote-http.js"); -const remoteHttpMock = vi.mocked(withRemoteHttpResponse); - -function jsonResponse(payload: unknown, status = 200): Response { - return { - ok: status >= 200 && status < 300, - status, - json: async () => payload, - text: async () => JSON.stringify(payload), - } as Response; -} - -function textResponse(body: string, status: number): Response { - return { - ok: status >= 200 && status < 300, - status, - json: async () => JSON.parse(body) as unknown, - text: async () => body, - } as Response; -} - -describe("postJson", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("parses JSON payload on successful response", async () => { - remoteHttpMock.mockImplementationOnce(async (params) => { - return await params.onResponse(jsonResponse({ data: [{ embedding: [1, 2] }] })); - }); - - const result = await postJson({ - url: "https://memory.example/v1/post", - headers: { Authorization: "Bearer test" }, - body: { input: ["x"] }, - errorPrefix: "post failed", - parse: (payload) => payload, - }); - - expect(result).toEqual({ data: [{ embedding: [1, 2] }] }); - }); - - it("attaches status to thrown error when requested", async () => { - remoteHttpMock.mockImplementationOnce(async (params) => { - return await params.onResponse(textResponse("bad gateway", 502)); - }); - - await expect( - postJson({ - url: "https://memory.example/v1/post", - headers: {}, - body: {}, - errorPrefix: "post failed", - attachStatus: true, - parse: () => ({}), - }), - ).rejects.toMatchObject({ - message: expect.stringContaining("post failed: 502 bad gateway"), - status: 502, - }); - }); -}); +import "../../../packages/memory-host-sdk/src/host/post-json.test.js";