Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
156 changes: 156 additions & 0 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4056,6 +4056,160 @@ jobs:
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

telegram-injection-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',telegram-injection-vitest,') || contains(format(',{0},', inputs.scenarios), ',telegram-injection,') }}
runs-on: ubuntu-latest
timeout-minutes: 45
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "telegram-injection"
DOCKER_CONFIG: ${{ github.workspace }}/.docker-config-telegram-injection
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/telegram-injection
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-telegram-injection"
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 Telegram injection live test
# Migrated from test/e2e/test-telegram-injection.sh. Preserves the
# real OpenShell sandbox boundary for shell metacharacter payloads,
# process-table leak checks, and validateName rejection probes.
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
run: |
set -euo pipefail
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/telegram-injection.test.ts \
--silent=false --reporter=default

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

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"
DOCKER_CONFIG: ${{ github.workspace }}/.docker-config-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: 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 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
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}"

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 @@ -4226,6 +4380,8 @@ jobs:
gateway-health-honest-vitest,
device-auth-health-vitest,
channels-add-remove-vitest,
telegram-injection-vitest,
openclaw-discord-pairing-vitest,
]
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
permissions:
Expand Down
188 changes: 188 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,188 @@
// 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,
} 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 config = await sandboxSh(
sandbox,
SANDBOX_NAME,
`python3 - <<'PY'\nimport json\ncfg=json.load(open('/sandbox/.openclaw/openclaw.json'))\naccount=(cfg.get('channels',{}).get('discord',{}).get('accounts',{}).get('default') or {})\nproxy=cfg.get('proxy') or {}\nprint(json.dumps({'token': account.get('token',''), 'dmPolicy': account.get('dmPolicy',''), 'allowFrom': account.get('allowFrom', []), 'accountProxy': account.get('proxy',''), 'managedProxy': proxy.get('proxyUrl','') if proxy.get('enabled') is True else ''}))\nPY`,
{ 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
Loading