feat(qa): wire crabline slack transport (#97891)

This commit is contained in:
Dallin Romney
2026-06-29 10:57:17 -07:00
committed by GitHub
parent bc7f0f1223
commit 2cc43aec2d
8 changed files with 122 additions and 18 deletions

View File

@@ -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": {

View File

@@ -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({

View File

@@ -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<string, unknown>;
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<string, unknown>;

View File

@@ -176,6 +176,7 @@ export type QaTransportAdapter = {
replyChannel: string;
replyTo: string;
};
createRuntimeEnvPatch?: () => NodeJS.ProcessEnv;
handleAction: (params: {
action: QaTransportActionName;
args: Record<string, unknown>;

View File

@@ -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"),

View File

@@ -1558,6 +1558,7 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise<QaSuite
forcedRuntime: params?.forcedRuntime,
mockBaseUrl: mock?.baseUrl,
}),
transport.createRuntimeEnvPatch?.(),
buildQaGatewayHeapCheckpointRuntimeEnvPatch(),
),
});

10
pnpm-lock.yaml generated
View File

@@ -1346,8 +1346,8 @@ importers:
version: 4.4.3
devDependencies:
'@openclaw/crabline':
specifier: 0.1.0
version: 0.1.0
specifier: 0.1.3
version: 0.1.3
'@openclaw/discord':
specifier: workspace:*
version: link:../discord
@@ -3182,8 +3182,8 @@ packages:
cpu: [x64]
os: [win32]
'@openclaw/crabline@0.1.0':
resolution: {integrity: sha512-9M63tpAN3UNJGpCPB5e8bSu5aS4AGHfcH11fTKoNWr6ieetprJRA0iqToMu/ZDu/j1P2yNnOQwQ0XDTTLeU0lw==}
'@openclaw/crabline@0.1.3':
resolution: {integrity: sha512-ZbwAnR8k/gFWBPgJyGdJlHdZ4prEm3upMpchKpbogIR9NclOzSyzQ8+ScOotdjWLBnMBldsiemECc50goA5YxQ==}
engines: {node: '>=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

View File

@@ -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"