From 2cc43aec2deb62c357e52d3f833fa9e38ef45020 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Mon, 29 Jun 2026 10:57:17 -0700 Subject: [PATCH] feat(qa): wire crabline slack transport (#97891) --- extensions/qa-lab/package.json | 2 +- .../qa-lab/src/crabline-transport.test.ts | 105 +++++++++++++++++- extensions/qa-lab/src/crabline-transport.ts | 16 +-- extensions/qa-lab/src/qa-transport.ts | 1 + extensions/qa-lab/src/suite.test.ts | 3 +- extensions/qa-lab/src/suite.ts | 1 + pnpm-lock.yaml | 10 +- pnpm-workspace.yaml | 2 +- 8 files changed, 122 insertions(+), 18 deletions(-) diff --git a/extensions/qa-lab/package.json b/extensions/qa-lab/package.json index 11dafbb6c1d9..c123d902df56 100644 --- a/extensions/qa-lab/package.json +++ b/extensions/qa-lab/package.json @@ -16,7 +16,7 @@ "@openclaw/plugin-sdk": "workspace:*", "@openclaw/slack": "workspace:*", "@openclaw/whatsapp": "workspace:*", - "@openclaw/crabline": "0.1.0", + "@openclaw/crabline": "0.1.3", "openclaw": "2026.5.28" }, "peerDependencies": { diff --git a/extensions/qa-lab/src/crabline-transport.test.ts b/extensions/qa-lab/src/crabline-transport.test.ts index 6bc327344e75..16d1d783af74 100644 --- a/extensions/qa-lab/src/crabline-transport.test.ts +++ b/extensions/qa-lab/src/crabline-transport.test.ts @@ -8,10 +8,10 @@ import { describe, expect, it } from "vitest"; import { createQaBusState } from "./bus-state.js"; import { createQaCrablineTransportAdapter } from "./crabline-transport.js"; -function createSelection() { +function createSelection(channel: "slack" | "telegram" = "telegram") { return { capabilityMatrixPath: "crabline-fake-provider-capabilities.json", - channel: "telegram", + channel, channelDriver: "crabline", smokeArtifactPath: "crabline-fake-provider-smoke.json", } as const; @@ -59,6 +59,107 @@ describe("crabline transport", () => { }); }); + it("configures OpenClaw's Slack plugin against a Crabline fake provider server", async () => { + await withTempDir("qa-crabline-transport-", async (outputDir) => { + const transport = await createQaCrablineTransportAdapter({ + outputDir, + selection: createSelection("slack"), + state: createQaBusState(), + }); + + try { + expect(transport.id).toBe("crabline"); + expect(transport.requiredPluginIds).toEqual(["slack"]); + expect(transport.createGatewayConfig({ baseUrl: "http://127.0.0.1:1" })).toMatchObject({ + channels: { + slack: { + botToken: "xoxb-crabline-slack-token", + enabled: true, + mode: "http", + signingSecret: "crabline-slack-signing-secret", + }, + }, + }); + expect(transport.createRuntimeEnvPatch?.()).toMatchObject({ + SLACK_API_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\/api\/$/u), + SLACK_BOT_TOKEN: "xoxb-crabline-slack-token", + SLACK_SIGNING_SECRET: "crabline-slack-signing-secret", + }); + + const manifest = JSON.parse( + await fs.readFile(path.join(outputDir, OPENCLAW_CRABLINE_MANIFEST_PATH), "utf8"), + ) as { + provider?: string; + }; + expect(manifest.provider).toBe("slack"); + } finally { + await transport.cleanup?.(); + } + }); + }); + + it("injects inbound messages through Crabline and mirrors Slack sends into normalized state", async () => { + await withTempDir("qa-crabline-transport-", async (outputDir) => { + const transport = await createQaCrablineTransportAdapter({ + outputDir, + selection: createSelection("slack"), + state: createQaBusState(), + }); + + try { + await transport.state.addInboundMessage({ + conversation: { + id: "D12345678", + kind: "direct", + }, + senderId: "U12345678", + senderName: "Alice", + text: "Slack baseline marker check.", + }); + + const env = transport.createRuntimeEnvPatch?.() ?? {}; + expect(env.SLACK_API_URL).toBeTruthy(); + expect(env.SLACK_BOT_TOKEN).toBeTruthy(); + const { response, release } = await fetchWithSsrFGuard({ + url: `${env.SLACK_API_URL}chat.postMessage`, + init: { + body: JSON.stringify({ + channel: "D12345678", + text: "assistant via fake slack", + }), + headers: { + authorization: `Bearer ${env.SLACK_BOT_TOKEN}`, + "content-type": "application/json", + }, + method: "POST", + }, + policy: { allowPrivateNetwork: true }, + auditContext: "qa-lab-crabline-slack-transport-test", + }); + await release(); + expect(response.ok).toBe(true); + + await expect( + transport.state.waitFor({ + direction: "outbound", + kind: "message-text", + textIncludes: "assistant via fake slack", + timeoutMs: 1_000, + }), + ).resolves.toMatchObject({ + conversation: { + id: "D12345678", + kind: "direct", + }, + direction: "outbound", + text: "assistant via fake slack", + }); + } finally { + await transport.cleanup?.(); + } + }); + }); + it("injects inbound messages through Crabline and mirrors Telegram sends into normalized state", async () => { await withTempDir("qa-crabline-transport-", async (outputDir) => { const transport = await createQaCrablineTransportAdapter({ diff --git a/extensions/qa-lab/src/crabline-transport.ts b/extensions/qa-lab/src/crabline-transport.ts index b26dc75a682f..dfcbb193d8a3 100644 --- a/extensions/qa-lab/src/crabline-transport.ts +++ b/extensions/qa-lab/src/crabline-transport.ts @@ -6,6 +6,7 @@ import { OPENCLAW_CRABLINE_MANIFEST_PATH, startOpenClawCrablineAdapter, type OpenClawCrablineChannelDriverSelection, + type OpenClawCrablineInbound, type StartedOpenClawCrablineAdapter, } from "@openclaw/crabline"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; @@ -96,16 +97,13 @@ async function waitForCrablineReady(params: { async function postCrablineInbound(params: { adapter: StartedOpenClawCrablineAdapter; - providerBody: Record; + providerInbound: OpenClawCrablineInbound; }) { const { response, release } = await fetchWithSsrFGuard({ - url: params.adapter.manifest.endpoints.adminInboundUrl, + url: params.providerInbound.providerUrl, init: { - body: JSON.stringify(params.providerBody), - headers: { - authorization: `Bearer ${params.adapter.manifest.adminToken}`, - "content-type": "application/json", - }, + body: JSON.stringify(params.providerInbound.providerBody), + headers: params.providerInbound.providerHeaders, method: "POST", }, policy: { allowPrivateNetwork: true }, @@ -190,7 +188,7 @@ function createCrablineState(params: { }); await postCrablineInbound({ adapter: params.adapter, - providerBody: providerInbound.providerBody, + providerInbound, }); return message; }, @@ -257,6 +255,8 @@ class QaCrablineTransport extends QaStateBackedTransportAdapter { return delivery; }; + createRuntimeEnvPatch = () => this.#adapter.createChannelDriverSmokeEnv({}); + handleAction = async (_params: { action: QaTransportActionName; args: Record; diff --git a/extensions/qa-lab/src/qa-transport.ts b/extensions/qa-lab/src/qa-transport.ts index 0c609df6da56..3f3cb7817957 100644 --- a/extensions/qa-lab/src/qa-transport.ts +++ b/extensions/qa-lab/src/qa-transport.ts @@ -176,6 +176,7 @@ export type QaTransportAdapter = { replyChannel: string; replyTo: string; }; + createRuntimeEnvPatch?: () => NodeJS.ProcessEnv; handleAction: (params: { action: QaTransportActionName; args: Record; diff --git a/extensions/qa-lab/src/suite.test.ts b/extensions/qa-lab/src/suite.test.ts index f52ddcc0144b..0781ff569fbc 100644 --- a/extensions/qa-lab/src/suite.test.ts +++ b/extensions/qa-lab/src/suite.test.ts @@ -1,6 +1,7 @@ // Qa Lab tests cover suite plugin behavior. import fs from "node:fs/promises"; import path from "node:path"; +import { CRABLINE_FAKE_PROVIDER_CHANNELS } from "@openclaw/crabline"; import { afterEach, describe, expect, it, vi } from "vitest"; import { QA_EVIDENCE_FILENAME, QA_EVIDENCE_SUMMARY_KIND } from "./evidence-summary.js"; import type { QaLabServerHandle } from "./lab-server.types.js"; @@ -386,7 +387,7 @@ describe("qa suite", () => { }; expect(matrix.report?.result).toMatchObject({ selectedChannel: "telegram", - supportedChannels: ["telegram"], + supportedChannels: [...CRABLINE_FAKE_PROVIDER_CHANNELS].toSorted(), }); const smoke = JSON.parse( await fs.readFile(path.join(outputDir, "crabline-fake-provider-smoke.json"), "utf8"), diff --git a/extensions/qa-lab/src/suite.ts b/extensions/qa-lab/src/suite.ts index 4527873651c7..026ea0b73bc8 100644 --- a/extensions/qa-lab/src/suite.ts +++ b/extensions/qa-lab/src/suite.ts @@ -1558,6 +1558,7 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise=22'} hasBin: true @@ -9269,7 +9269,7 @@ snapshots: '@openai/codex@0.139.0-win32-x64': optional: true - '@openclaw/crabline@0.1.0': + '@openclaw/crabline@0.1.3': dependencies: commander: 14.0.3 picocolors: 1.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 532333962e00..3a55be116734 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,7 +7,7 @@ packages: minimumReleaseAge: 2880 minimumReleaseAgeExclude: - - "@openclaw/crabline@0.1.0" + - "@openclaw/crabline@0.1.3" - "@openclaw/fs-safe@0.3.0" - "@openclaw/proxyline@0.3.3" - "acpx"