Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
98ccb03
test(e2e): migrate Telegram injection to Vitest
cv Jun 21, 2026
7a1acdf
test(e2e): keep Telegram injection test linear
cv Jun 22, 2026
60b0526
test(e2e): migrate OpenClaw Discord pairing to Vitest
cv Jun 22, 2026
27e79b0
test(e2e): harden Slack policy host check
cv Jun 22, 2026
5c1a6ff
test(e2e): address Discord pairing review
jyaunches Jun 22, 2026
30cab27
test(e2e): fix Discord pairing CI
jyaunches Jun 22, 2026
3eca115
test(e2e): fix Discord WS framing
jyaunches Jun 22, 2026
aff8347
test(e2e): keep legacy capture redacted
jyaunches Jun 22, 2026
b1f23d8
test(e2e): avoid conditional guardrail
jyaunches Jun 22, 2026
10a7e0f
test(e2e): extend workflow matrix timeout
jyaunches Jun 22, 2026
5f84adb
test(e2e): avoid multiline sandbox command
jyaunches Jun 22, 2026
126ed1f
test(e2e): tighten Discord runtime probe
jyaunches Jun 22, 2026
f1c6309
Merge remote-tracking branch 'origin/main' into e2e-phase6-openclaw-d…
jyaunches Jun 22, 2026
df65f31
ci(e2e): scrub Discord installer env
jyaunches Jun 22, 2026
47d23f2
test(e2e): narrow Discord helper surface
jyaunches Jun 22, 2026
47a3f5c
test(e2e): avoid source-shape assertion
jyaunches Jun 22, 2026
cccb4f4
test(e2e): block Discord workflow API secret
jyaunches Jun 22, 2026
4730b53
test(e2e): harden Discord workflow boundary
jyaunches Jun 22, 2026
d45e00f
test(e2e): preserve Discord proof CRLF
jyaunches Jun 22, 2026
ca3d938
Merge remote-tracking branch 'origin/main' into e2e-phase6-openclaw-d…
jyaunches Jun 22, 2026
225bafd
Merge remote-tracking branch 'origin/main' into e2e-phase6-openclaw-d…
jyaunches Jun 22, 2026
79c7167
test(e2e): split Discord workflow boundary
jyaunches Jun 22, 2026
f2498d9
test(e2e): align Discord proof transport
jyaunches Jun 22, 2026
b9ce55b
Merge remote-tracking branch 'origin/main' into e2e-phase6-openclaw-d…
jyaunches Jun 22, 2026
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
112 changes: 112 additions & 0 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4542,6 +4542,116 @@ jobs:
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

openclaw-discord-pairing-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',openclaw-discord-pairing-vitest,') || contains(format(',{0},', inputs.scenarios), ',openclaw-discord-pairing,') }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "openclaw-discord-pairing"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/openclaw-discord-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-discord-pairing"
OPENSHELL_GATEWAY: "nemoclaw"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Configure isolated Docker auth directory
run: echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-openclaw-discord-pairing" >> "$GITHUB_ENV"

- 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}"
login_succeeded=0
for attempt in 1 2 3; do
if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then
login_succeeded=1
break
fi
if [[ "$attempt" -lt 3 ]]; then
echo "::warning::Docker Hub login attempt ${attempt} failed; retrying."
sleep 5
fi
done
if [[ "$login_succeeded" -ne 1 ]]; then
echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls."
fi

- 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: Install OpenShell CLI
run: |
set -euo pipefail
env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh

- name: Run OpenClaw Discord pairing live test
# Migrated from test/e2e/test-openclaw-discord-pairing.sh. Preserves
# fake Discord Gateway token rewrite and connect-shell pairing approval.
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
DISCORD_BOT_TOKEN: test-fake-discord-pairing-e2e
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
if command -v openshell >/dev/null 2>&1; then
OPENSHELL_BIN="$(command -v openshell)"
elif [ -x "$HOME/.local/bin/openshell" ]; then
OPENSHELL_BIN="$HOME/.local/bin/openshell"
else
echo "::error::OpenShell CLI not found after install"
ls -la /usr/local/bin/openshell "$HOME/.local/bin/openshell" 2>&1 || true
exit 1
fi
export OPENSHELL_BIN
echo "Using OPENSHELL_BIN=$OPENSHELL_BIN"
"$OPENSHELL_BIN" --version
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/openclaw-discord-pairing.test.ts \
--silent=false --reporter=default

- name: Upload OpenClaw Discord pairing artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-vitest-scenarios-openclaw-discord-pairing
path: e2e-artifacts/vitest/openclaw-discord-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}"

tunnel-lifecycle-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',tunnel-lifecycle-vitest,') || contains(format(',{0},', inputs.scenarios), ',tunnel-lifecycle,') }}
Expand Down Expand Up @@ -4657,6 +4767,7 @@ jobs:
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"


# ── PR result comment (mirrors nightly-e2e.yaml's report-to-pr) ───────────
# Posts a results table on the open PR for the dispatching branch (or the
# PR identified by `inputs.pr_number`). `if: always()` so the comment lands
Expand Down Expand Up @@ -4728,6 +4839,7 @@ jobs:
channels-add-remove-vitest,
tunnel-lifecycle-vitest,
telegram-injection-vitest,
openclaw-discord-pairing-vitest,
channels-stop-start-vitest,
]
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
Expand Down
191 changes: 191 additions & 0 deletions test/e2e-scenario/live/openclaw-discord-pairing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// 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-discord-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,
DISCORD_DM_CHANNEL,
extractPairingResult,
issuePairingRequest,
PAIRING_USER,
pairingEnv,
pairingRedactions,
runDiscordGatewayProof,
startFakeDiscordGateway,
writePairingArtifacts,
} from "./openclaw-pairing-helpers.ts";
import {
dockerInfo,
expectExitZero,
expectSandboxReady,
installSandboxOrSkipOnRateLimit,
resultText,
sandboxSh,
shellQuote,
} from "./phase6-messaging-helpers.ts";

const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-openclaw-discord-pairing";
const DISCORD_TOKEN = process.env.DISCORD_BOT_TOKEN ?? "test-fake-discord-pairing-e2e";
const LIVE_TIMEOUT_MS = 55 * 60_000;

function assertDiscordGatewayCapture(captureFile: string, expectedToken: string): void {
const rows = fs
.readFileSync(captureFile, "utf8")
.trim()
.split(/\n+/)
.filter(Boolean)
.map((line) => JSON.parse(line) as Record<string, unknown>);
const identify = rows.filter((row) => row.event === "identify").at(-1);
expect(identify, "fake Discord Gateway did not capture IDENTIFY").toBeTruthy();
expect(identify).not.toHaveProperty("token");
expect(JSON.stringify(rows), "fake Discord Gateway capture persisted raw token").not.toContain(
expectedToken,
);
expect(identify?.tokenMatchesExpected, "Discord token rewrite").toBe(true);
expect(identify?.tokenLooksPlaceholder, "Discord placeholder leaked").toBe(false);
}

test.skipIf(!shouldRunLiveE2EScenarios())(
"OpenClaw Discord 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: "discord",
discordToken: DISCORD_TOKEN,
});
const redactions = pairingRedactions({ apiKey, discordToken: DISCORD_TOKEN });

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

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

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

const install = await installSandboxOrSkipOnRateLimit(
host,
env,
redactions,
"install-discord-pairing",
skip,
"NVIDIA endpoint validation was rate-limited before Discord pairing assertions ran",
);
expectExitZero(install, "install.sh --non-interactive with Discord");
await expectSandboxReady(host, SANDBOX_NAME, env, redactions, "sandbox-list-discord-pairing");

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

const configScript =
"import json; cfg=json.load(open('/sandbox/.openclaw/openclaw.json')); account=(cfg.get('channels',{}).get('discord',{}).get('accounts',{}).get('default') or {}); proxy=cfg.get('proxy') or {}; print(json.dumps({'token': account.get('token',''), 'dmPolicy': account.get('dmPolicy',''), 'allowFrom': account.get('allowFrom', []), 'accountProxy': account.get('proxy',''), 'managedProxy': proxy.get('proxyUrl','')}))";
const config = await sandboxSh(
sandbox,
SANDBOX_NAME,
`python3 -c ${shellQuote(configScript)}`,
{ artifactName: "discord-openclaw-config", redactionValues: redactions },
);
expectExitZero(config, "Discord OpenClaw config");
const configSummary = JSON.parse(config.stdout.trim()) as {
token: string;
dmPolicy: string;
allowFrom: string[];
accountProxy: string;
managedProxy: string;
};
expect(configSummary.token).toContain("openshell:resolve:env:");
expect(configSummary.token).toContain("DISCORD_BOT_TOKEN");
expect(configSummary.dmPolicy).not.toBe("allowlist");
expect(configSummary.accountProxy, "Discord account proxy").toMatch(/^http:\/\//);
expect(configSummary.managedProxy, "OpenClaw managed proxy").toMatch(/^http:\/\//);

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

const fakeGateway = await startFakeDiscordGateway(
host,
cleanup,
env,
DISCORD_TOKEN,
redactions,
);
await applyFakePolicy({
host,
sandboxName: SANDBOX_NAME,
api: fakeGateway,
protocol: "websocket",
rewrite: "websocket-credential-rewrite",
env,
redactions,
artifactName: "apply-discord-gateway-policy",
});
const gatewayProof = await runDiscordGatewayProof({
sandbox,
sandboxName: SANDBOX_NAME,
port: fakeGateway.port,
redactions,
});
expectExitZero(gatewayProof, "Discord Gateway protocol proof");
expect(resultText(gatewayProof)).toContain("UPGRADE");
expect(resultText(gatewayProof)).toContain("HELLO");
expect(resultText(gatewayProof)).toContain("IDENTIFY_SENT_PLACEHOLDER");
expect(resultText(gatewayProof)).toContain("READY");
expect(resultText(gatewayProof)).toContain("HEARTBEAT_ACK");
assertDiscordGatewayCapture(fakeGateway.captureFile, DISCORD_TOKEN);

const issue = await issuePairingRequest({
sandbox,
sandboxName: SANDBOX_NAME,
channel: "discord",
redactions,
});
expectExitZero(issue, "Discord pairing request creation");
const pairing = extractPairingResult(resultText(issue), "DISCORD_PAIRING_E2E_RESULT");
expect(pairing.senderId).toBe(PAIRING_USER.discord);
expect(pairing.channelId).toBe(DISCORD_DM_CHANNEL);
expect(pairing.replyText, "Discord pairing reply includes generated code").toContain(
pairing.code,
);
expect(pairing.replyText, "Discord pairing reply includes sender identity").toContain(
PAIRING_USER.discord,
);
await writePairingArtifacts(artifacts, "discord", { ...pairing, user: PAIRING_USER.discord });

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