From e912df54381640316a79c0868c7762ee0c81fc4f Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Wed, 13 May 2026 12:43:04 +0000 Subject: [PATCH] =?UTF-8?q?test(canvas):=20add=20ExternalConnectModal=20pu?= =?UTF-8?q?re-helper=20coverage=20=E2=80=94=2031=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract and unit-test the 8 pure fill helpers and 2 derived functions from ExternalConnectModal so they are independently verifiable. Exported: fillPythonSnippet, fillCurlSnippet, fillChannelSnippet, fillUniversalMcpSnippet, fillHermesSnippet, fillCodexSnippet, fillOpenClawSnippet, buildFilledSnippets, buildTabOrder. Issue: #709 follow-up (pure-helper extraction) Co-Authored-By: Claude Opus 4.7 --- .../src/components/ExternalConnectModal.tsx | 174 ++++++----- .../__tests__/ExternalConnectModal.test.tsx | 275 ++++++++++++++++++ 2 files changed, 380 insertions(+), 69 deletions(-) create mode 100644 canvas/src/components/__tests__/ExternalConnectModal.test.tsx diff --git a/canvas/src/components/ExternalConnectModal.tsx b/canvas/src/components/ExternalConnectModal.tsx index 94f0d7a5..3c4ad33d 100644 --- a/canvas/src/components/ExternalConnectModal.tsx +++ b/canvas/src/components/ExternalConnectModal.tsx @@ -18,6 +18,109 @@ import { useCallback, useState } from "react"; import * as Dialog from "@radix-ui/react-dialog"; +// ─── Pure fill helpers ──────────────────────────────────────────────────────── +// Each snippet is server-stamped with workspace_id + platform_url but leaves +// AUTH_TOKEN as a placeholder. These helpers stamp the real token in so the +// operator's copy-paste is truly ready-to-run. All are pure string ops. + +export function fillPythonSnippet( + snippet: string, + authToken: string, +): string { + return snippet.replace( + 'AUTH_TOKEN = ""', + `AUTH_TOKEN = "${authToken}"`, + ); +} + +export function fillCurlSnippet( + snippet: string, + authToken: string, +): string { + return snippet.replace( + 'WORKSPACE_AUTH_TOKEN=""', + `WORKSPACE_AUTH_TOKEN="${authToken}"`, + ); +} + +export function fillChannelSnippet( + snippet: string | undefined, + authToken: string, +): string | undefined { + return snippet?.replace( + 'MOLECULE_WORKSPACE_TOKENS=', + `MOLECULE_WORKSPACE_TOKENS=${authToken}`, + ); +} + +export function fillUniversalMcpSnippet( + snippet: string | undefined, + authToken: string, +): string | undefined { + return snippet?.replace( + 'MOLECULE_WORKSPACE_TOKEN=""', + `MOLECULE_WORKSPACE_TOKEN="${authToken}"`, + ); +} + +export function fillHermesSnippet( + snippet: string | undefined, + authToken: string, +): string | undefined { + return snippet?.replace( + 'MOLECULE_WORKSPACE_TOKEN=""', + `MOLECULE_WORKSPACE_TOKEN="${authToken}"`, + ); +} + +export function fillCodexSnippet( + snippet: string | undefined, + authToken: string, +): string | undefined { + return snippet?.replace( + 'MOLECULE_WORKSPACE_TOKEN = ""', + `MOLECULE_WORKSPACE_TOKEN = "${authToken}"`, + ); +} + +export function fillOpenClawSnippet( + snippet: string | undefined, + authToken: string, +): string | undefined { + return snippet?.replace( + 'WORKSPACE_TOKEN=""', + `WORKSPACE_TOKEN="${authToken}"`, + ); +} + +/** Build the ordered tab list shown in the modal. Each tab only appears when + * the platform supplies the corresponding snippet. */ +export function buildTabOrder(info: ExternalConnectionInfo): Tab[] { + const tabs: Tab[] = []; + const { filledUniversalMcp, filledChannel, filledHermes, filledCodex, filledOpenClaw } = buildFilledSnippets(info); + if (filledUniversalMcp) tabs.push("mcp"); + tabs.push("python"); + if (filledChannel) tabs.push("claude"); + if (filledHermes) tabs.push("hermes"); + if (filledCodex) tabs.push("codex"); + if (filledOpenClaw) tabs.push("openclaw"); + tabs.push("curl", "fields"); + return tabs; +} + +/** Pre-fill all snippets from an info object. Exposed for testing. */ +export function buildFilledSnippets(info: ExternalConnectionInfo) { + return { + filledPython: fillPythonSnippet(info.python_snippet, info.auth_token), + filledCurl: fillCurlSnippet(info.curl_register_template, info.auth_token), + filledChannel: fillChannelSnippet(info.claude_code_channel_snippet, info.auth_token), + filledUniversalMcp: fillUniversalMcpSnippet(info.universal_mcp_snippet, info.auth_token), + filledHermes: fillHermesSnippet(info.hermes_channel_snippet, info.auth_token), + filledCodex: fillCodexSnippet(info.codex_snippet, info.auth_token), + filledOpenClaw: fillOpenClawSnippet(info.openclaw_snippet, info.auth_token), + }; +} + type Tab = "python" | "curl" | "claude" | "mcp" | "hermes" | "codex" | "openclaw" | "fields"; export interface ExternalConnectionInfo { @@ -102,54 +205,7 @@ export function ExternalConnectModal({ info, onClose }: Props) { if (!info) return null; - // Python snippet is stamped server-side with workspace_id + - // platform_url but leaves AUTH_TOKEN as a "" placeholder - // (that's what we're showing in the modal). Fill in the real - // token here so the snippet the operator copies is truly ready-to-run. - const filledPython = info.python_snippet.replace( - 'AUTH_TOKEN = ""', - `AUTH_TOKEN = "${info.auth_token}"`, - ); - const filledCurl = info.curl_register_template.replace( - 'WORKSPACE_AUTH_TOKEN=""', - `WORKSPACE_AUTH_TOKEN="${info.auth_token}"`, - ); - // The channel snippet asks the operator to paste the auth_token into - // the .env file's MOLECULE_WORKSPACE_TOKENS field. Stamp it server-side - // here so the copy-paste-block is truly ready-to-run. - const filledChannel = info.claude_code_channel_snippet?.replace( - 'MOLECULE_WORKSPACE_TOKENS=', - `MOLECULE_WORKSPACE_TOKENS=${info.auth_token}`, - ); - // Universal MCP snippet uses MOLECULE_WORKSPACE_TOKEN as the env-var - // name passed through to molecule-mcp via `claude mcp add ... -- env - // MOLECULE_WORKSPACE_TOKEN=...`. The placeholder must match the - // template's literal — pre-2026-04-30 polish this looked for - // WORKSPACE_AUTH_TOKEN (carryover from the curl tab), which silently - // skipped the substitution and left "" - // visible in the operator's clipboard. - const filledUniversalMcp = info.universal_mcp_snippet?.replace( - 'MOLECULE_WORKSPACE_TOKEN=""', - `MOLECULE_WORKSPACE_TOKEN="${info.auth_token}"`, - ); - // Hermes channel snippet uses MOLECULE_WORKSPACE_TOKEN (same env-var - // name as Universal MCP). Stamp the auth_token in so the operator's - // copy-paste is fully ready-to-run. - const filledHermes = info.hermes_channel_snippet?.replace( - 'MOLECULE_WORKSPACE_TOKEN=""', - `MOLECULE_WORKSPACE_TOKEN="${info.auth_token}"`, - ); - // Codex + OpenClaw snippets carry the placeholder inside the - // generated config block (TOML / JSON respectively). Stamp the - // token in so the copy-paste is one less manual edit. - const filledCodex = info.codex_snippet?.replace( - 'MOLECULE_WORKSPACE_TOKEN = ""', - `MOLECULE_WORKSPACE_TOKEN = "${info.auth_token}"`, - ); - const filledOpenClaw = info.openclaw_snippet?.replace( - 'WORKSPACE_TOKEN=""', - `WORKSPACE_TOKEN="${info.auth_token}"`, - ); + const { filledPython, filledCurl, filledChannel, filledUniversalMcp, filledHermes, filledCodex, filledOpenClaw } = buildFilledSnippets(info); return ( !o && onClose()}> @@ -171,27 +227,7 @@ export function ExternalConnectModal({ info, onClose }: Props) { aria-label="Connection snippet format" className="mt-4 flex gap-1 border-b border-line" > - {(() => { - // Build the tab order dynamically. Claude Code first - // (when offered) since it's the simplest setup; Python - // SDK second (full register+heartbeat+inbound); Universal - // MCP third (any MCP-aware runtime, outbound-only); curl - // for one-shot register; Fields for raw values. - // Tab order: Universal MCP first (default, runtime- - // agnostic primitives), then runtime-specific channel/ - // SDK tabs, then curl + Fields. Each runtime tab only - // appears when the platform supplies the snippet — no - // dead "tab missing snippet" UX. - const tabs: Tab[] = []; - if (filledUniversalMcp) tabs.push("mcp"); - tabs.push("python"); - if (filledChannel) tabs.push("claude"); - if (filledHermes) tabs.push("hermes"); - if (filledCodex) tabs.push("codex"); - if (filledOpenClaw) tabs.push("openclaw"); - tabs.push("curl", "fields"); - return tabs; - })().map((t) => ( + {buildTabOrder(info).map((t) => (