Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4210,6 +4210,84 @@ jobs:
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

openclaw-slack-pairing-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',openclaw-slack-pairing-vitest,') || contains(format(',{0},', inputs.scenarios), ',openclaw-slack-pairing,') }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "openclaw-slack-pairing"
DOCKER_CONFIG: ${{ github.workspace }}/.docker-config-openclaw-slack-pairing
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/openclaw-slack-pairing
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-openclaw-slack-pairing"
OPENSHELL_GATEWAY: "nemoclaw"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Authenticate to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then
echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls."
exit 0
fi
mkdir -p "${DOCKER_CONFIG}"
chmod 700 "${DOCKER_CONFIG}"
echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin || echo "::warning::Docker Hub login failed; continuing with anonymous pulls."

- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
cache: npm

- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Build CLI
run: npm run build:cli

- name: Run OpenClaw Slack pairing live test
# Migrated from test/e2e/test-openclaw-slack-pairing.sh. Preserves
# fake Slack Socket Mode/REST token rewrite and connect-shell approval.
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
SLACK_BOT_TOKEN: xoxb-fake-slack-pairing-e2e
SLACK_APP_TOKEN: xapp-fake-slack-pairing-e2e
run: |
set -euo pipefail
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/openclaw-slack-pairing.test.ts \
--silent=false --reporter=default

- name: Upload OpenClaw Slack pairing artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-vitest-scenarios-openclaw-slack-pairing
path: e2e-artifacts/vitest/openclaw-slack-pairing/
include-hidden-files: false
if-no-files-found: ignore
retention-days: 14

- name: Clean up Docker auth
if: always()
run: |
set -euo pipefail
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

issue-2478-crash-loop-recovery-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',issue-2478-crash-loop-recovery-vitest,') || contains(format(',{0},', inputs.scenarios), ',issue-2478-crash-loop-recovery,') }}
Expand Down Expand Up @@ -4382,6 +4460,7 @@ jobs:
channels-add-remove-vitest,
telegram-injection-vitest,
openclaw-discord-pairing-vitest,
openclaw-slack-pairing-vitest,
]
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
permissions:
Expand Down
173 changes: 173 additions & 0 deletions test/e2e-scenario/live/openclaw-slack-pairing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** Live Vitest replacement for test/e2e/test-openclaw-slack-pairing.sh. */

import fs from "node:fs";

import { expect, test } from "../fixtures/e2e-test.ts";
import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts";
import {
applyFakePolicy,
approveAndAssertPairing,
assertOpenClawStateRoot,
cleanupPairingSandbox,
extractPairingCode,
issuePairingRequest,
PAIRING_USER,
pairingEnv,
pairingRedactions,
premergeSlackPolicyIfNeeded,
startFakeSlackApi,
writePairingArtifacts,
} from "./openclaw-pairing-helpers.ts";
import {
dockerInfo,
expectExitZero,
expectSandboxReady,
installSandbox,
resultText,
} from "./phase6-messaging-helpers.ts";

const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-slack-pairing";
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN ?? "xoxb-fake-slack-pairing-e2e";
const SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN ?? "xapp-fake-slack-pairing-e2e";
const LIVE_TIMEOUT_MS = 55 * 60_000;

function assertSlackCapture(captureFile: string): void {
const rows = fs
.readFileSync(captureFile, "utf8")
.trim()
.split(/\n+/)
.filter(Boolean)
.map((line) => JSON.parse(line) as Record<string, unknown>);
const ws = rows
.filter(
(row) => row.event === "websocket-message" && row.messageType === "socket_mode_client_hello",
)
.at(-1);
expect(ws, "fake Slack did not capture Socket Mode hello").toBeTruthy();
expect(ws?.tokenMatchesExpected, "Slack xapp websocket token rewrite").toBe(true);
expect(ws?.tokenLooksPlaceholder, "Slack xapp placeholder leaked").toBe(false);

const post = rows
.filter((row) => row.event === "request" && row.path === "/api/chat.postMessage")
.at(-1);
expect(post, "fake Slack did not capture chat.postMessage").toBeTruthy();
expect(post?.authorization, "raw Slack authorization should not be captured").toBeUndefined();
expect(post?.body, "raw Slack body should not be captured").toBeUndefined();
expect(post?.tokenMatchesExpected, "Slack xoxb auth rewrite").toBe(true);
expect(post?.bodyMatchesExpected, "Slack xoxb body rewrite").toBe(true);
expect(post?.tokenLooksPlaceholder, "Slack xoxb placeholder leaked").toBe(false);
}

test.skipIf(!shouldRunLiveE2EScenarios())(
"OpenClaw Slack Socket Mode pairing request is shared with connect-shell approval",
{ timeout: LIVE_TIMEOUT_MS },
async ({ artifacts, cleanup, host, sandbox, secrets, skip }) => {
const apiKey = secrets.required("NVIDIA_INFERENCE_API_KEY");
const env = pairingEnv({
sandboxName: SANDBOX_NAME,
apiKey,
channel: "slack",
slackBot: SLACK_BOT_TOKEN,
slackApp: SLACK_APP_TOKEN,
});
const redactions = pairingRedactions({
apiKey,
slackBot: SLACK_BOT_TOKEN,
slackApp: SLACK_APP_TOKEN,
});

await artifacts.writeJson("scenario.json", {
id: "openclaw-slack-pairing",
legacySource: "test/e2e/test-openclaw-slack-pairing.sh",
boundary:
"install.sh Slack OpenClaw sandbox + fake Slack REST/websocket token rewrite + runtime pairing request + connect-shell approval",
sandboxName: SANDBOX_NAME,
pairingUser: PAIRING_USER.slack,
});

cleanup.add(`destroy Slack pairing sandbox ${SANDBOX_NAME}`, () =>
cleanupPairingSandbox(host, SANDBOX_NAME, env, redactions, "cleanup-slack-pairing"),
);
await premergeSlackPolicyIfNeeded(cleanup);
await cleanupPairingSandbox(host, SANDBOX_NAME, env, redactions, "preclean-slack-pairing");

const docker = await dockerInfo(host, env);
expect(docker.exitCode, resultText(docker)).toBe(0);

try {
const install = await installSandbox(host, env, redactions, "install-slack-pairing");
expectExitZero(install, "install.sh --non-interactive with Slack");
} catch (error) {
if (String(error).includes("NVIDIA_ENDPOINT_RATE_LIMIT")) {
skip("NVIDIA endpoint validation was rate-limited before Slack pairing assertions ran");
return;
}
throw error;
}
await expectSandboxReady(host, SANDBOX_NAME, env, redactions, "sandbox-list-slack-pairing");

for (const providerName of [`${SANDBOX_NAME}-slack-bridge`, `${SANDBOX_NAME}-slack-app`]) {
const provider = await host.command("openshell", ["provider", "get", providerName], {
artifactName: `provider-get-${providerName}`,
env,
redactionValues: redactions,
timeoutMs: 60_000,
});
expectExitZero(provider, `${providerName} exists`);
}

await assertOpenClawStateRoot(sandbox, SANDBOX_NAME, "slack", redactions);

const fakeSlack = await startFakeSlackApi(
host,
cleanup,
env,
SLACK_BOT_TOKEN,
SLACK_APP_TOKEN,
redactions,
);
await applyFakePolicy({
host,
sandboxName: SANDBOX_NAME,
api: fakeSlack,
protocol: "rest",
rewrite: "request-body-credential-rewrite",
env,
redactions,
artifactName: "apply-slack-rest-policy",
});
await applyFakePolicy({
host,
sandboxName: SANDBOX_NAME,
api: fakeSlack,
protocol: "websocket",
rewrite: "websocket-credential-rewrite",
env,
redactions,
artifactName: "apply-slack-websocket-policy",
});

const issue = await issuePairingRequest({
sandbox,
sandboxName: SANDBOX_NAME,
channel: "slack",
redactions,
fakeSlackPort: fakeSlack.port,
});
expectExitZero(issue, "Slack pairing request creation");
const code = extractPairingCode(resultText(issue), "PAIRING_E2E_RESULT");
assertSlackCapture(fakeSlack.captureFile);
await writePairingArtifacts(artifacts, "slack", { code, user: PAIRING_USER.slack });

await approveAndAssertPairing({
sandbox,
sandboxName: SANDBOX_NAME,
channel: "slack",
code,
redactions,
});
},
);
Loading