Skip to content

Commit 5619798

Browse files
committed
feat(qa): wire crabline slack transport
1 parent 0deddfa commit 5619798

8 files changed

Lines changed: 122 additions & 18 deletions

File tree

extensions/qa-lab/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@openclaw/plugin-sdk": "workspace:*",
1717
"@openclaw/slack": "workspace:*",
1818
"@openclaw/whatsapp": "workspace:*",
19-
"@openclaw/crabline": "0.1.0",
19+
"@openclaw/crabline": "0.1.3",
2020
"openclaw": "2026.5.28"
2121
},
2222
"peerDependencies": {

extensions/qa-lab/src/crabline-transport.test.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { describe, expect, it } from "vitest";
88
import { createQaBusState } from "./bus-state.js";
99
import { createQaCrablineTransportAdapter } from "./crabline-transport.js";
1010

11-
function createSelection() {
11+
function createSelection(channel: "slack" | "telegram" = "telegram") {
1212
return {
1313
capabilityMatrixPath: "crabline-fake-provider-capabilities.json",
14-
channel: "telegram",
14+
channel,
1515
channelDriver: "crabline",
1616
smokeArtifactPath: "crabline-fake-provider-smoke.json",
1717
} as const;
@@ -59,6 +59,107 @@ describe("crabline transport", () => {
5959
});
6060
});
6161

62+
it("configures OpenClaw's Slack plugin against a Crabline fake provider server", async () => {
63+
await withTempDir("qa-crabline-transport-", async (outputDir) => {
64+
const transport = await createQaCrablineTransportAdapter({
65+
outputDir,
66+
selection: createSelection("slack"),
67+
state: createQaBusState(),
68+
});
69+
70+
try {
71+
expect(transport.id).toBe("crabline");
72+
expect(transport.requiredPluginIds).toEqual(["slack"]);
73+
expect(transport.createGatewayConfig({ baseUrl: "http://127.0.0.1:1" })).toMatchObject({
74+
channels: {
75+
slack: {
76+
botToken: "xoxb-crabline-slack-token",
77+
enabled: true,
78+
mode: "http",
79+
signingSecret: "crabline-slack-signing-secret",
80+
},
81+
},
82+
});
83+
expect(transport.createRuntimeEnvPatch?.()).toMatchObject({
84+
SLACK_API_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\/api\/$/u),
85+
SLACK_BOT_TOKEN: "xoxb-crabline-slack-token",
86+
SLACK_SIGNING_SECRET: "crabline-slack-signing-secret",
87+
});
88+
89+
const manifest = JSON.parse(
90+
await fs.readFile(path.join(outputDir, OPENCLAW_CRABLINE_MANIFEST_PATH), "utf8"),
91+
) as {
92+
provider?: string;
93+
};
94+
expect(manifest.provider).toBe("slack");
95+
} finally {
96+
await transport.cleanup?.();
97+
}
98+
});
99+
});
100+
101+
it("injects inbound messages through Crabline and mirrors Slack sends into normalized state", async () => {
102+
await withTempDir("qa-crabline-transport-", async (outputDir) => {
103+
const transport = await createQaCrablineTransportAdapter({
104+
outputDir,
105+
selection: createSelection("slack"),
106+
state: createQaBusState(),
107+
});
108+
109+
try {
110+
await transport.state.addInboundMessage({
111+
conversation: {
112+
id: "D12345678",
113+
kind: "direct",
114+
},
115+
senderId: "U12345678",
116+
senderName: "Alice",
117+
text: "Slack baseline marker check.",
118+
});
119+
120+
const env = transport.createRuntimeEnvPatch?.() ?? {};
121+
expect(env.SLACK_API_URL).toBeTruthy();
122+
expect(env.SLACK_BOT_TOKEN).toBeTruthy();
123+
const { response, release } = await fetchWithSsrFGuard({
124+
url: `${env.SLACK_API_URL}chat.postMessage`,
125+
init: {
126+
body: JSON.stringify({
127+
channel: "D12345678",
128+
text: "assistant via fake slack",
129+
}),
130+
headers: {
131+
authorization: `Bearer ${env.SLACK_BOT_TOKEN}`,
132+
"content-type": "application/json",
133+
},
134+
method: "POST",
135+
},
136+
policy: { allowPrivateNetwork: true },
137+
auditContext: "qa-lab-crabline-slack-transport-test",
138+
});
139+
await release();
140+
expect(response.ok).toBe(true);
141+
142+
await expect(
143+
transport.state.waitFor({
144+
direction: "outbound",
145+
kind: "message-text",
146+
textIncludes: "assistant via fake slack",
147+
timeoutMs: 1_000,
148+
}),
149+
).resolves.toMatchObject({
150+
conversation: {
151+
id: "D12345678",
152+
kind: "direct",
153+
},
154+
direction: "outbound",
155+
text: "assistant via fake slack",
156+
});
157+
} finally {
158+
await transport.cleanup?.();
159+
}
160+
});
161+
});
162+
62163
it("injects inbound messages through Crabline and mirrors Telegram sends into normalized state", async () => {
63164
await withTempDir("qa-crabline-transport-", async (outputDir) => {
64165
const transport = await createQaCrablineTransportAdapter({

extensions/qa-lab/src/crabline-transport.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
OPENCLAW_CRABLINE_MANIFEST_PATH,
77
startOpenClawCrablineAdapter,
88
type OpenClawCrablineChannelDriverSelection,
9+
type OpenClawCrablineInbound,
910
type StartedOpenClawCrablineAdapter,
1011
} from "@openclaw/crabline";
1112
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -96,16 +97,13 @@ async function waitForCrablineReady(params: {
9697

9798
async function postCrablineInbound(params: {
9899
adapter: StartedOpenClawCrablineAdapter;
99-
providerBody: Record<string, unknown>;
100+
providerInbound: OpenClawCrablineInbound;
100101
}) {
101102
const { response, release } = await fetchWithSsrFGuard({
102-
url: params.adapter.manifest.endpoints.adminInboundUrl,
103+
url: params.providerInbound.providerUrl,
103104
init: {
104-
body: JSON.stringify(params.providerBody),
105-
headers: {
106-
authorization: `Bearer ${params.adapter.manifest.adminToken}`,
107-
"content-type": "application/json",
108-
},
105+
body: JSON.stringify(params.providerInbound.providerBody),
106+
headers: params.providerInbound.providerHeaders,
109107
method: "POST",
110108
},
111109
policy: { allowPrivateNetwork: true },
@@ -190,7 +188,7 @@ function createCrablineState(params: {
190188
});
191189
await postCrablineInbound({
192190
adapter: params.adapter,
193-
providerBody: providerInbound.providerBody,
191+
providerInbound,
194192
});
195193
return message;
196194
},
@@ -257,6 +255,8 @@ class QaCrablineTransport extends QaStateBackedTransportAdapter {
257255
return delivery;
258256
};
259257

258+
createRuntimeEnvPatch = () => this.#adapter.createChannelDriverSmokeEnv({});
259+
260260
handleAction = async (_params: {
261261
action: QaTransportActionName;
262262
args: Record<string, unknown>;

extensions/qa-lab/src/qa-transport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export type QaTransportAdapter = {
176176
replyChannel: string;
177177
replyTo: string;
178178
};
179+
createRuntimeEnvPatch?: () => NodeJS.ProcessEnv;
179180
handleAction: (params: {
180181
action: QaTransportActionName;
181182
args: Record<string, unknown>;

extensions/qa-lab/src/suite.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Qa Lab tests cover suite plugin behavior.
22
import fs from "node:fs/promises";
33
import path from "node:path";
4+
import { CRABLINE_FAKE_PROVIDER_CHANNELS } from "@openclaw/crabline";
45
import { afterEach, describe, expect, it, vi } from "vitest";
56
import { QA_EVIDENCE_FILENAME, QA_EVIDENCE_SUMMARY_KIND } from "./evidence-summary.js";
67
import type { QaLabServerHandle } from "./lab-server.types.js";
@@ -386,7 +387,7 @@ describe("qa suite", () => {
386387
};
387388
expect(matrix.report?.result).toMatchObject({
388389
selectedChannel: "telegram",
389-
supportedChannels: ["telegram"],
390+
supportedChannels: [...CRABLINE_FAKE_PROVIDER_CHANNELS].toSorted(),
390391
});
391392
const smoke = JSON.parse(
392393
await fs.readFile(path.join(outputDir, "crabline-fake-provider-smoke.json"), "utf8"),

extensions/qa-lab/src/suite.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,7 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise<QaSuite
15581558
forcedRuntime: params?.forcedRuntime,
15591559
mockBaseUrl: mock?.baseUrl,
15601560
}),
1561+
transport.createRuntimeEnvPatch?.(),
15611562
buildQaGatewayHeapCheckpointRuntimeEnvPatch(),
15621563
),
15631564
});

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ packages:
77
minimumReleaseAge: 2880
88

99
minimumReleaseAgeExclude:
10-
- "@openclaw/[email protected].0"
10+
- "@openclaw/[email protected].3"
1111
- "@openclaw/[email protected]"
1212
- "@openclaw/[email protected]"
1313
- "acpx"

0 commit comments

Comments
 (0)