From 64755c52f2f15ecd455f8cf73e3de16442126ec9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 10:55:47 +0100 Subject: [PATCH] test: move extension-owned coverage out of core --- .../src/brave-web-search-provider.test.ts | 53 ++++++++ extensions/google/web-search-provider.test.ts | 35 ++++++ ...sion-conversation.bundled-fallback.test.ts | 105 ++++++++++++++++ .../plugins/session-conversation.test.ts | 53 +------- src/config/config.web-search-provider.test.ts | 99 +++++++++++---- src/flows/search-setup.test.ts | 99 +++++++++++++++ src/secrets/runtime.integration.test.ts | 113 ++---------------- .../session-conversation-registry.ts | 50 +++++--- 8 files changed, 410 insertions(+), 197 deletions(-) create mode 100644 extensions/google/web-search-provider.test.ts create mode 100644 src/channels/plugins/session-conversation.bundled-fallback.test.ts diff --git a/extensions/brave/src/brave-web-search-provider.test.ts b/extensions/brave/src/brave-web-search-provider.test.ts index 0ac703417cc..996c4599249 100644 --- a/extensions/brave/src/brave-web-search-provider.test.ts +++ b/extensions/brave/src/brave-web-search-provider.test.ts @@ -1,6 +1,14 @@ +import fs from "node:fs"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js"; import { __testing, createBraveWebSearchProvider } from "./brave-web-search-provider.js"; +const braveManifest = JSON.parse( + fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"), +) as { + configSchema?: Record; +}; + describe("brave web search provider", () => { const priorFetch = global.fetch; @@ -58,6 +66,51 @@ describe("brave web search provider", () => { expect(__testing.resolveBraveMode({ mode: "llm-context" })).toBe("llm-context"); }); + it("accepts llm-context in the Brave plugin config schema", () => { + if (!braveManifest.configSchema) { + throw new Error("Expected Brave manifest config schema"); + } + + const result = validateJsonSchemaValue({ + schema: braveManifest.configSchema, + cacheKey: "test:brave-config-schema", + value: { + webSearch: { + mode: "llm-context", + }, + }, + }); + + expect(result.ok).toBe(true); + }); + + it("rejects invalid Brave mode values in the plugin config schema", () => { + if (!braveManifest.configSchema) { + throw new Error("Expected Brave manifest config schema"); + } + + const result = validateJsonSchemaValue({ + schema: braveManifest.configSchema, + cacheKey: "test:brave-config-schema", + value: { + webSearch: { + mode: "invalid-mode", + }, + }, + }); + + expect(result.ok).toBe(false); + if (result.ok) { + return; + } + expect(result.errors).toContainEqual( + expect.objectContaining({ + path: "webSearch.mode", + allowedValues: ["web", "llm-context"], + }), + ); + }); + it("maps llm-context results into wrapped source entries", () => { expect( __testing.mapBraveLlmContextResults({ diff --git a/extensions/google/web-search-provider.test.ts b/extensions/google/web-search-provider.test.ts new file mode 100644 index 00000000000..0b62e1a7bd1 --- /dev/null +++ b/extensions/google/web-search-provider.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../src/config/config.js"; +import { withEnv } from "../../test/helpers/plugins/env.js"; +import { __testing, createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js"; + +describe("google web search provider", () => { + it("falls back to GEMINI_API_KEY from the environment", () => { + withEnv({ GEMINI_API_KEY: "AIza-env-test" }, () => { + expect(__testing.resolveGeminiApiKey()).toBe("AIza-env-test"); + }); + }); + + it("prefers configured api keys over env fallbacks", () => { + withEnv({ GEMINI_API_KEY: "AIza-env-test" }, () => { + expect(__testing.resolveGeminiApiKey({ apiKey: "AIza-configured-test" })).toBe( + "AIza-configured-test", + ); + }); + }); + + it("stores configured credentials at the canonical plugin config path", () => { + const provider = createGeminiWebSearchProvider(); + const config = {} as OpenClawConfig; + + provider.setConfiguredCredentialValue?.(config, "AIza-plugin-test"); + + expect(provider.credentialPath).toBe("plugins.entries.google.config.webSearch.apiKey"); + expect(provider.getConfiguredCredentialValue?.(config)).toBe("AIza-plugin-test"); + }); + + it("defaults the Gemini web search model and trims explicit overrides", () => { + expect(__testing.resolveGeminiModel()).toBe("gemini-2.5-flash"); + expect(__testing.resolveGeminiModel({ model: " gemini-2.5-pro " })).toBe("gemini-2.5-pro"); + }); +}); diff --git a/src/channels/plugins/session-conversation.bundled-fallback.test.ts b/src/channels/plugins/session-conversation.bundled-fallback.test.ts new file mode 100644 index 00000000000..d2044059bd0 --- /dev/null +++ b/src/channels/plugins/session-conversation.bundled-fallback.test.ts @@ -0,0 +1,105 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../../config/config.js"; +import { resetPluginRuntimeStateForTest } from "../../plugins/runtime.js"; + +const fallbackState = vi.hoisted(() => ({ + activeDirName: null as string | null, + resolveSessionConversation: null as + | ((params: { kind: "group" | "channel"; rawId: string }) => { + id: string; + threadId?: string | null; + baseConversationId?: string | null; + parentConversationCandidates?: string[]; + } | null) + | null, +})); + +vi.mock("../../plugin-sdk/facade-runtime.js", () => ({ + tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) => + dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation + ? { resolveSessionConversation: fallbackState.resolveSessionConversation } + : null, +})); + +vi.mock("../../plugins/bundled-plugin-metadata.js", () => ({ + resolveBundledPluginPublicSurfacePath: ({ dirName }: { dirName: string }) => + dirName === fallbackState.activeDirName ? `/tmp/${dirName}/session-key-api.js` : null, +})); + +import { resolveSessionConversationRef } from "./session-conversation.js"; + +describe("session conversation bundled fallback", () => { + beforeEach(() => { + fallbackState.activeDirName = null; + fallbackState.resolveSessionConversation = null; + resetPluginRuntimeStateForTest(); + }); + + afterEach(() => { + clearRuntimeConfigSnapshot(); + }); + + it("delegates pre-bootstrap thread parsing to the active bundled channel plugin", () => { + fallbackState.activeDirName = "mock-threaded"; + fallbackState.resolveSessionConversation = ({ rawId }) => { + const [conversationId, threadId] = rawId.split(":topic:"); + return { + id: conversationId, + threadId, + baseConversationId: conversationId, + parentConversationCandidates: [conversationId], + }; + }; + setRuntimeConfigSnapshot({ + plugins: { + entries: { + "mock-threaded": { + enabled: true, + }, + }, + }, + }); + + expect(resolveSessionConversationRef("agent:main:mock-threaded:group:room:topic:42")).toEqual({ + channel: "mock-threaded", + kind: "group", + rawId: "room:topic:42", + id: "room", + threadId: "42", + baseSessionKey: "agent:main:mock-threaded:group:room", + baseConversationId: "room", + parentConversationCandidates: ["room"], + }); + }); + + it("uses explicit bundled parent candidates before registry bootstrap", () => { + fallbackState.activeDirName = "mock-parent"; + fallbackState.resolveSessionConversation = ({ rawId }) => ({ + id: rawId, + baseConversationId: "room", + parentConversationCandidates: ["room:topic:root", "room"], + }); + setRuntimeConfigSnapshot({ + plugins: { + entries: { + "mock-parent": { + enabled: true, + }, + }, + }, + }); + + expect( + resolveSessionConversationRef("agent:main:mock-parent:group:room:topic:root:sender:user"), + ).toEqual({ + channel: "mock-parent", + kind: "group", + rawId: "room:topic:root:sender:user", + id: "room:topic:root:sender:user", + threadId: undefined, + baseSessionKey: "agent:main:mock-parent:group:room:topic:root:sender:user", + baseConversationId: "room", + parentConversationCandidates: ["room:topic:root", "room"], + }); + }); +}); diff --git a/src/channels/plugins/session-conversation.test.ts b/src/channels/plugins/session-conversation.test.ts index ea0ed5ae688..668334c4a4c 100644 --- a/src/channels/plugins/session-conversation.test.ts +++ b/src/channels/plugins/session-conversation.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../../config/config.js"; +import { clearRuntimeConfigSnapshot } from "../../config/config.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { createSessionConversationTestRegistry } from "../../test-utils/session-conversation-registry.js"; @@ -54,57 +54,6 @@ describe("session conversation routing", () => { ); }); - it("keeps bundled Telegram topic parsing available before registry bootstrap", () => { - resetPluginRuntimeStateForTest(); - setRuntimeConfigSnapshot({ - channels: { - telegram: { - enabled: true, - }, - }, - }); - - expect(resolveSessionConversationRef("agent:main:telegram:group:-100123:topic:77")).toEqual({ - channel: "telegram", - kind: "group", - rawId: "-100123:topic:77", - id: "-100123", - threadId: "77", - baseSessionKey: "agent:main:telegram:group:-100123", - baseConversationId: "-100123", - parentConversationCandidates: ["-100123"], - }); - }); - - it("keeps bundled Feishu parent fallbacks available before registry bootstrap", () => { - resetPluginRuntimeStateForTest(); - setRuntimeConfigSnapshot({ - plugins: { - entries: { - feishu: { - enabled: true, - }, - }, - }, - }); - - expect( - resolveSessionConversationRef( - "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", - ), - ).toEqual({ - channel: "feishu", - kind: "group", - rawId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user", - id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user", - threadId: undefined, - baseSessionKey: - "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", - baseConversationId: "oc_group_chat", - parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"], - }); - }); - it("does not load bundled session-key fallbacks for inactive channel plugins", () => { resetPluginRuntimeStateForTest(); diff --git a/src/config/config.web-search-provider.test.ts b/src/config/config.web-search-provider.test.ts index 46e6c042db8..f193b2aaf92 100644 --- a/src/config/config.web-search-provider.test.ts +++ b/src/config/config.web-search-provider.test.ts @@ -91,10 +91,83 @@ vi.mock("../plugins/web-search-providers.js", () => { }; }); +const secretInputSchema = { + oneOf: [ + { type: "string" }, + { + type: "object", + additionalProperties: false, + properties: { + source: { type: "string" }, + provider: { type: "string" }, + id: { type: "string" }, + }, + required: ["source", "provider", "id"], + }, + ], +}; + +function buildWebSearchPluginSchema() { + return { + type: "object", + additionalProperties: false, + properties: { + webSearch: { + type: "object", + additionalProperties: false, + properties: { + apiKey: secretInputSchema, + baseUrl: secretInputSchema, + model: { type: "string" }, + }, + }, + }, + }; +} + +vi.mock("../plugins/manifest-registry.js", () => ({ + loadPluginManifestRegistry: () => ({ + plugins: [ + { + id: "brave", + origin: "bundled", + channels: [], + providers: [], + cliBackends: [], + skills: [], + hooks: [], + rootDir: "/tmp/plugins/brave", + source: "test", + manifestPath: "/tmp/plugins/brave/openclaw.plugin.json", + schemaCacheKey: "test:brave", + configSchema: buildWebSearchPluginSchema(), + }, + ...["firecrawl", "google", "moonshot", "perplexity", "searxng", "tavily", "xai"].map( + (id) => ({ + id, + origin: "bundled", + channels: [], + providers: [], + cliBackends: [], + skills: [], + hooks: [], + rootDir: `/tmp/plugins/${id}`, + source: "test", + manifestPath: `/tmp/plugins/${id}/openclaw.plugin.json`, + schemaCacheKey: `test:${id}`, + configSchema: buildWebSearchPluginSchema(), + }), + ), + ], + diagnostics: [], + }), +})); + let validateConfigObjectWithPlugins: typeof import("./config.js").validateConfigObjectWithPlugins; let resolveSearchProvider: typeof import("../agents/tools/web-search.js").__testing.resolveSearchProvider; beforeAll(async () => { + vi.resetModules(); ({ validateConfigObjectWithPlugins } = await import("./config.js")); ({ __testing: { resolveSearchProvider }, @@ -257,32 +330,6 @@ describe("web search provider config", () => { expect(res.ok).toBe(true); }); - - it("accepts brave llm-context mode config", () => { - const res = validateConfigObjectWithPlugins( - buildWebSearchProviderConfig({ - provider: "brave", - providerConfig: { - mode: "llm-context", - }, - }), - ); - - expect(res.ok).toBe(true); - }); - - it("rejects invalid brave mode config values", () => { - const res = validateConfigObjectWithPlugins( - buildWebSearchProviderConfig({ - provider: "brave", - providerConfig: { - mode: "invalid-mode", - }, - }), - ); - - expect(res.ok).toBe(false); - }); }); describe("web search provider auto-detection", () => { diff --git a/src/flows/search-setup.test.ts b/src/flows/search-setup.test.ts index 0e081e2edb3..2af0a0383aa 100644 --- a/src/flows/search-setup.test.ts +++ b/src/flows/search-setup.test.ts @@ -3,6 +3,105 @@ import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js"; import { createNonExitingRuntime } from "../runtime.js"; import { runSearchSetupFlow } from "./search-setup.js"; +const mockGrokProvider = vi.hoisted(() => ({ + id: "grok", + pluginId: "xai", + label: "Grok", + hint: "Search with xAI", + docsUrl: "https://docs.openclaw.ai/tools/web", + requiresCredential: true, + credentialLabel: "xAI API key", + placeholder: "xai-...", + signupUrl: "https://x.ai/api", + envVars: ["XAI_API_KEY"], + onboardingScopes: ["text-inference"], + credentialPath: "plugins.entries.xai.config.webSearch.apiKey", + getCredentialValue: (search?: Record) => search?.apiKey, + setCredentialValue: (searchConfigTarget: Record, value: unknown) => { + searchConfigTarget.apiKey = value; + }, + getConfiguredCredentialValue: (config?: Record) => + ( + config?.plugins as + | { + entries?: Record< + string, + { + config?: { + webSearch?: { apiKey?: unknown }; + }; + } + >; + } + | undefined + )?.entries?.xai?.config?.webSearch?.apiKey, + setConfiguredCredentialValue: (configTarget: Record, value: unknown) => { + const plugins = (configTarget.plugins ??= {}) as Record; + const entries = (plugins.entries ??= {}) as Record; + const xaiEntry = (entries.xai ??= {}) as Record; + const xaiConfig = (xaiEntry.config ??= {}) as Record; + const webSearch = (xaiConfig.webSearch ??= {}) as Record; + webSearch.apiKey = value; + }, + runSetup: async ({ + config, + prompter, + }: { + config: Record; + prompter: { select: (params: Record) => Promise }; + }) => { + const enableXSearch = await prompter.select({ + message: "Enable x_search", + options: [ + { value: "yes", label: "Yes" }, + { value: "no", label: "No" }, + ], + }); + if (enableXSearch !== "yes") { + return config; + } + const model = await prompter.select({ + message: "Grok model", + options: [{ value: "grok-4-1-fast", label: "grok-4-1-fast" }], + }); + const pluginEntries = (config.plugins as { entries?: Record } | undefined) + ?.entries; + const existingXaiEntry = pluginEntries?.xai as Record | undefined; + const existingXaiConfig = ( + pluginEntries?.xai as { config?: Record } | undefined + )?.config; + return { + ...config, + plugins: { + ...(config.plugins as Record | undefined), + entries: { + ...pluginEntries, + xai: { + ...existingXaiEntry, + config: { + ...existingXaiConfig, + xSearch: { + enabled: true, + model, + }, + }, + }, + }, + }, + }; + }, +})); + +vi.mock("../plugins/bundled-web-search.js", () => ({ + listBundledWebSearchProviders: () => [mockGrokProvider], + resolveBundledWebSearchPluginId: (providerId: string | undefined) => + providerId === "grok" ? "xai" : undefined, +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ + resolvePluginWebSearchProviders: () => [mockGrokProvider], +})); + describe("runSearchSetupFlow", () => { it("runs provider-owned setup after selecting Grok web search", async () => { const select = vi diff --git a/src/secrets/runtime.integration.test.ts b/src/secrets/runtime.integration.test.ts index d3921a83db2..382c1b196b4 100644 --- a/src/secrets/runtime.integration.test.ts +++ b/src/secrets/runtime.integration.test.ts @@ -11,11 +11,15 @@ import { writeConfigFile, } from "../config/config.js"; import { withTempHome } from "../config/home-env.test-harness.js"; +import { clearPluginDiscoveryCache } from "../plugins/discovery.js"; +import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js"; +import { __testing as webFetchProvidersTesting } from "../plugins/web-fetch-providers.runtime.js"; +import { __testing as webSearchProvidersTesting } from "../plugins/web-search-providers.runtime.js"; import { captureEnv, withEnvAsync } from "../test-utils/env.js"; import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot, - getActiveRuntimeWebToolsMetadata, getActiveSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot, } from "./runtime.js"; @@ -119,6 +123,7 @@ describe("secrets runtime snapshot integration", () => { beforeEach(() => { envSnapshot = captureEnv([ "OPENCLAW_BUNDLED_PLUGINS_DIR", + "OPENCLAW_DISABLE_BUNDLED_PLUGINS", "OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE", "OPENCLAW_VERSION", ]); @@ -133,6 +138,11 @@ describe("secrets runtime snapshot integration", () => { clearSecretsRuntimeSnapshot(); clearRuntimeConfigSnapshot(); clearConfigCache(); + clearPluginLoaderCache(); + clearPluginDiscoveryCache(); + clearPluginManifestRegistryCache(); + webSearchProvidersTesting.resetWebSearchProviderSnapshotCacheForTests(); + webFetchProvidersTesting.resetWebFetchProviderSnapshotCacheForTests(); }); it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => { @@ -346,107 +356,6 @@ describe("secrets runtime snapshot integration", () => { SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS, ); - it( - "keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", - async () => { - await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => { - const prepared = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - web: { - search: { - provider: "gemini", - }, - }, - }, - plugins: { - entries: { - google: { - config: { - webSearch: { - apiKey: { - source: "env", - provider: "default", - id: "WEB_SEARCH_GEMINI_API_KEY", - }, - }, - }, - }, - }, - }, - }), - env: { - WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - activateSecretsRuntimeSnapshot(prepared); - - await expect( - writeConfigFile({ - ...loadConfig(), - plugins: { - entries: { - google: { - config: { - webSearch: { - apiKey: { - source: "env", - provider: "default", - id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", - }, - }, - }, - }, - }, - }, - tools: { - web: { - search: { - provider: "gemini", - }, - }, - }, - }), - ).rejects.toThrow( - /runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i, - ); - - const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); - expect(activeAfterFailure).not.toBeNull(); - const loadedGoogleWebSearchConfig = loadConfig().plugins?.entries?.google?.config as - | { webSearch?: { apiKey?: unknown } } - | undefined; - expect(loadedGoogleWebSearchConfig?.webSearch?.apiKey).toBe( - "web-search-gemini-runtime-key", - ); - const activeSourceGoogleWebSearchConfig = activeAfterFailure?.sourceConfig.plugins?.entries - ?.google?.config as { webSearch?: { apiKey?: unknown } } | undefined; - expect(activeSourceGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "WEB_SEARCH_GEMINI_API_KEY", - }); - expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini"); - - const persistedConfig = JSON.parse( - await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), - ) as OpenClawConfig; - const persistedGoogleWebSearchConfig = persistedConfig.plugins?.entries?.google?.config as - | { webSearch?: { apiKey?: unknown } } - | undefined; - expect(persistedGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", - }); - }); - }, - SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS, - ); - it("recomputes config-derived agent dirs when refreshing active secrets runtime snapshots", async () => { await withTempHome("openclaw-secrets-runtime-agent-dirs-", async (home) => { const mainAgentDir = path.join(home, ".openclaw", "agents", "main", "agent"); diff --git a/src/test-utils/session-conversation-registry.ts b/src/test-utils/session-conversation-registry.ts index 2f11bc8b2d1..6fc37519638 100644 --- a/src/test-utils/session-conversation-registry.ts +++ b/src/test-utils/session-conversation-registry.ts @@ -1,24 +1,40 @@ -import { loadBundledPluginPublicSurfaceSync } from "./bundled-plugin-public-surface.js"; import { createTestRegistry } from "./channel-plugins.js"; -type SessionConversationSurface = { - resolveSessionConversation?: (params: { kind: "group" | "channel"; rawId: string }) => { - id: string; - threadId?: string | null; - baseConversationId?: string | null; - parentConversationCandidates?: string[]; - } | null; -}; - -function loadSessionConversationSurface(pluginId: string) { - return loadBundledPluginPublicSurfaceSync({ - pluginId, - artifactBasename: "session-key-api.js", - }).resolveSessionConversation; +function resolveTelegramSessionConversation(params: { kind: "group" | "channel"; rawId: string }) { + if (params.kind !== "group") { + return null; + } + const match = params.rawId.match(/^(?.+):topic:(?[^:]+)$/u); + if (!match?.groups?.chatId || !match.groups.topicId) { + return null; + } + const chatId = match.groups.chatId; + return { + id: chatId, + threadId: match.groups.topicId, + baseConversationId: chatId, + parentConversationCandidates: [chatId], + }; } -const resolveTelegramSessionConversation = loadSessionConversationSurface("telegram"); -const resolveFeishuSessionConversation = loadSessionConversationSurface("feishu"); +function resolveFeishuSessionConversation(params: { kind: "group" | "channel"; rawId: string }) { + if (params.kind !== "group") { + return null; + } + const senderMatch = params.rawId.match( + /^(?[^:]+):topic:(?[^:]+):sender:(?[^:]+)$/u, + ); + if (!senderMatch?.groups?.chatId || !senderMatch.groups.topicId || !senderMatch.groups.senderId) { + return null; + } + const chatId = senderMatch.groups.chatId; + const topicId = senderMatch.groups.topicId; + return { + id: params.rawId, + baseConversationId: chatId, + parentConversationCandidates: [`${chatId}:topic:${topicId}`, chatId], + }; +} export function createSessionConversationTestRegistry() { return createTestRegistry([