diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 519b460846a..ce8f3d99ca8 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -371,11 +371,11 @@ jobs: path: /tmp/nemoclaw-e2e-messaging-compatible-endpoint-install.log if-no-files-found: ignore - # ── Channels stop/start lifecycle E2E (#3462 Test 1) ───────── - # Regression coverage for #3453 (channels stop must actually disable the - # channel across rebuild) and #3381 (channels start must re-attach from - # the cached credential). Telegram-only — Discord/Slack walk the same - # disabledChannels filter; telegram is the cheapest regression gate. + # ── Channels stop/start/remove lifecycle E2E (#3462, #3671) ───────── + # Regression coverage for #3453 (stop must disable across rebuild), #3381 + # (start must re-attach from cached credentials), and #3671 (remove must + # detach/delete providers and survive rebuild with token env still present). + # Exercises OpenClaw and Hermes across telegram, discord, wechat, and slack. channels-stop-start-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && @@ -383,14 +383,14 @@ jobs: inputs.jobs == '' || contains(format(',{0},', inputs.jobs), ',channels-stop-start-e2e,')) runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 120 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} - - name: Run channels stop/start lifecycle E2E test + - name: Run channels stop/start/remove lifecycle E2E test env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} NEMOCLAW_NON_INTERACTIVE: "1" @@ -400,6 +400,18 @@ jobs: GITHUB_TOKEN: ${{ github.token }} TELEGRAM_BOT_TOKEN: "test-fake-telegram-token-stop-start-e2e" TELEGRAM_ALLOWED_IDS: "123456789" + DISCORD_BOT_TOKEN: "test-fake-discord-token-stop-start-e2e" + DISCORD_SERVER_ID: "1491590992753590594" + DISCORD_ALLOWED_IDS: "1005536447329222676" + DISCORD_REQUIRE_MENTION: "0" + SLACK_BOT_TOKEN: "xoxb-fake-slack-token-stop-start-e2e" + SLACK_APP_TOKEN: "xapp-fake-slack-app-token-stop-start-e2e" + SLACK_ALLOWED_USERS: "U0123456789,U09ABCDEFGH" + WECHAT_BOT_TOKEN: "test-fake-wechat-token-stop-start-e2e" + WECHAT_ACCOUNT_ID: "e2e-fake-account-stop-start" + WECHAT_BASE_URL: "https://ilinkai-fake-stop-start.wechat.com" + WECHAT_USER_ID: "wxid_stopstart_operator" + WECHAT_ALLOWED_IDS: "wxid_stopstart_operator" run: bash test/e2e/test-channels-stop-start.sh - name: Upload install log on failure @@ -407,7 +419,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: install-log-channels-stop-start - path: /tmp/nemoclaw-e2e-install.log + path: | + /tmp/nemoclaw-e2e-install.log + /tmp/nemoclaw-e2e-channels-*-install.log + /tmp/nc-channels-*.log if-no-files-found: ignore # ── Brave Search E2E (#2687) ───────────────────────────────── diff --git a/src/lib/actions/sandbox/policy-channel.ts b/src/lib/actions/sandbox/policy-channel.ts index dbb25cf35f9..bd4caf98444 100644 --- a/src/lib/actions/sandbox/policy-channel.ts +++ b/src/lib/actions/sandbox/policy-channel.ts @@ -337,12 +337,47 @@ async function applyChannelRemoveToGatewayAndRegistry( ); process.exit(1); } + + // Detach providers from the sandbox before deletion. openshell rejects + // `provider delete` with FailedPrecondition when the provider is still + // attached to a sandbox; the sandbox image itself only stops referencing + // the bridge after the next rebuild, so without an explicit detach the + // delete will fail on any sandbox that is still alive at remove-time. + // NotFound / NotAttached are treated as success-equivalent because a + // previous run may have already detached, or the channel may have been + // configured for a sandbox that is no longer alive. + const detachFailures: Array<{ name: string; output: string }> = []; + for (const envKey of channelTokenKeys) { + const name = bridgeProviderName(sandboxName, channelName, envKey); + const result = runOpenshell(["sandbox", "provider", "detach", sandboxName, name], { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + const output = `${result.stdout || ""}${result.stderr || ""}`; + if (!/\bNotFound\b|not found|not attached/i.test(output)) { + detachFailures.push({ name, output: output.trim() }); + } + } + } + if (detachFailures.length > 0) { + console.error( + ` Failed to detach bridge provider(s) from sandbox '${sandboxName}': ${detachFailures.map((f) => f.name).join(", ")}.`, + ); + for (const f of detachFailures) { + console.error(` [${f.name}] ${f.output.split("\n").join("\n ")}`); + } + console.error(" Registry not updated; re-run after resolving the gateway error."); + process.exit(1); + } + // Capture each delete's outcome. If any non-NotFound failure surfaces // we must NOT update the registry — otherwise NemoClaw would record // the channel as removed locally while the bridge is still live in // the gateway, which produces a half-configured sandbox the user - // can't easily recover. - const failed: string[] = []; + // can't easily recover. Surface the underlying openshell output so the + // operator can see exactly why the delete was rejected. + const deleteFailures: Array<{ name: string; output: string }> = []; for (const envKey of channelTokenKeys) { const name = bridgeProviderName(sandboxName, channelName, envKey); const result = runOpenshell(["provider", "delete", name], { @@ -354,14 +389,17 @@ async function applyChannelRemoveToGatewayAndRegistry( // Treat "not found" as success-equivalent — a previous run may // have already deleted the provider. if (!/\bNotFound\b|not found/i.test(output)) { - failed.push(name); + deleteFailures.push({ name, output: output.trim() }); } } } - if (failed.length > 0) { + if (deleteFailures.length > 0) { console.error( - ` Failed to delete bridge provider(s) from the OpenShell gateway: ${failed.join(", ")}.`, + ` Failed to delete bridge provider(s) from the OpenShell gateway: ${deleteFailures.map((f) => f.name).join(", ")}.`, ); + for (const f of deleteFailures) { + console.error(` [${f.name}] ${f.output.split("\n").join("\n ")}`); + } console.error(" Registry not updated; re-run after resolving the gateway error."); process.exit(1); } @@ -631,6 +669,40 @@ function applyChannelPresetIfAvailable(sandboxName: string, channelName: string) } } +// Mirror of applyChannelPresetIfAvailable. When the channel-named built-in +// preset is currently applied to the sandbox, un-apply it so `policy-list` +// no longer reports it active and the L7 proxy stops allow-listing the +// channel's upstream API (defense-in-depth: bridge is gone, egress to +// api.telegram.org / discord.com / slack.com should follow). Warns but does +// not abort the remove flow — the bridge teardown has already succeeded; +// the operator can run `policy-remove ` manually if cleanup falters. +function removeChannelPresetIfPresent(sandboxName: string, channelName: string): void { + const builtinPresets = new Set(policies.listPresets().map((p) => p.name)); + if (!builtinPresets.has(channelName)) { + return; + } + if (!policies.getAppliedPresets(sandboxName).includes(channelName)) { + return; + } + try { + const removed = policies.removePreset(sandboxName, channelName); + if (!removed) { + console.error( + ` ${YW}⚠${R} Channel '${channelName}' bridge removed but its policy preset failed to un-apply.`, + ); + console.error( + ` Run manually after rebuild with: ${CLI_NAME} ${sandboxName} policy-remove ${channelName}`, + ); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(` ${YW}⚠${R} Failed to remove '${channelName}' policy preset: ${msg}`); + console.error( + ` Run manually after rebuild with: ${CLI_NAME} ${sandboxName} policy-remove ${channelName}`, + ); + } +} + export async function removeSandboxChannel(sandboxName: string, args: string[] = []): Promise { const dryRun = args.includes("--dry-run"); const rawChannelArg = args.find((arg) => !arg.startsWith("-")); @@ -664,6 +736,9 @@ export async function removeSandboxChannel(sandboxName: string, args: string[] = getChannelTokenKeys(channel), ); console.log(` ${G}✓${R} Removed ${canonical} bridge from the OpenShell gateway.`); + + removeChannelPresetIfPresent(sandboxName, canonical); + await promptAndRebuild(sandboxName, `remove '${canonical}'`); } diff --git a/src/lib/onboard/messaging-reuse.test.ts b/src/lib/onboard/messaging-reuse.test.ts index 98b3c45c3cb..34a3a7abb99 100644 --- a/src/lib/onboard/messaging-reuse.test.ts +++ b/src/lib/onboard/messaging-reuse.test.ts @@ -81,7 +81,7 @@ describe("onboard messaging reuse", () => { expect(reusedChannels).toEqual(["wechat"]); }); - it("normalizes empty resume messaging channels to null", () => { + it("honors an explicit empty resume messaging channel set", () => { const reusedChannels = getNonInteractiveStoredMessagingChannels( true, ["unknown"], @@ -94,6 +94,22 @@ describe("onboard messaging reuse", () => { true, ); - expect(reusedChannels).toBeNull(); + expect(reusedChannels).toEqual([]); + }); + + it("does not rediscover token-backed channels when resume recorded none", () => { + const reusedChannels = getNonInteractiveStoredMessagingChannels( + true, + [], + "assistant", + messagingChannels, + () => true, + () => ({ messagingChannels: ["discord"] }), + () => [], + () => true, + true, + ); + + expect(reusedChannels).toEqual([]); }); }); diff --git a/src/lib/onboard/messaging-reuse.ts b/src/lib/onboard/messaging-reuse.ts index a4f454d6ca0..54f500342bf 100644 --- a/src/lib/onboard/messaging-reuse.ts +++ b/src/lib/onboard/messaging-reuse.ts @@ -35,7 +35,7 @@ export function getNonInteractiveStoredMessagingChannels( if (!nonInteractive) return null; if (resume && Array.isArray(sessionChannels)) { const knownSessionChannels = getKnownMessagingChannels(sessionChannels, messagingChannels); - return knownSessionChannels.length > 0 ? knownSessionChannels : null; + return knownSessionChannels; } if (resume || !sandboxName || messagingChannels.some((channel) => hasMessagingToken(channel.envKey))) { return null; diff --git a/test/e2e/docs/parity-inventory.generated.json b/test/e2e/docs/parity-inventory.generated.json index 780cb82fa89..1ced50b5f5f 100644 --- a/test/e2e/docs/parity-inventory.generated.json +++ b/test/e2e/docs/parity-inventory.generated.json @@ -231,376 +231,10 @@ }, { "script": "test/e2e/test-channels-stop-start.sh", - "assertions": [ - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 154, - "text": "C0: NVIDIA_API_KEY is required", - "polarity": "fail", - "normalized_id": "c0.nvidia.api.key.is.required", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 157, - "text": "C0: NVIDIA_API_KEY is set", - "polarity": "pass", - "normalized_id": "c0.nvidia.api.key.is.set", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 160, - "text": "C0: NEMOCLAW_NON_INTERACTIVE=1 is required", - "polarity": "fail", - "normalized_id": "c0.nemoclaw.non.interactive.1.is.required", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 163, - "text": "C0: NEMOCLAW_NON_INTERACTIVE=1 is set", - "polarity": "pass", - "normalized_id": "c0.nemoclaw.non.interactive.1.is.set", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 181, - "text": "C1a: Pre-cleanup complete", - "polarity": "pass", - "normalized_id": "c1a.pre.cleanup.complete", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 223, - "text": "C1b: install.sh + onboard completed (exit 0)", - "polarity": "pass", - "normalized_id": "c1b.install.sh.onboard.completed.exit.0", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 225, - "text": "C1b: install.sh failed (exit $install_exit)", - "polarity": "fail", - "normalized_id": "c1b.install.sh.failed.exit.install.exit", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 231, - "text": "C1c: openshell not on PATH after install", - "polarity": "fail", - "normalized_id": "c1c.openshell.not.on.path.after.install", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 234, - "text": "C1c: openshell installed", - "polarity": "pass", - "normalized_id": "c1c.openshell.installed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 237, - "text": "C1d: nemoclaw not on PATH after install", - "polarity": "fail", - "normalized_id": "c1d.nemoclaw.not.on.path.after.install", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 240, - "text": "C1d: nemoclaw installed", - "polarity": "pass", - "normalized_id": "c1d.nemoclaw.installed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 243, - "text": "C1e: Sandbox '${SANDBOX_NAME}' is Ready", - "polarity": "pass", - "normalized_id": "c1e.sandbox.sandbox.name.is.ready", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 245, - "text": "C1e: Sandbox '${SANDBOX_NAME}' not Ready", - "polarity": "fail", - "normalized_id": "c1e.sandbox.sandbox.name.not.ready", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 255, - "text": "C2a: Provider '${SANDBOX_NAME}-telegram-bridge' exists in gateway", - "polarity": "pass", - "normalized_id": "c2a.provider.sandbox.name.telegram.bridge.exists.in.gateway", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 257, - "text": "C2a: Provider '${SANDBOX_NAME}-telegram-bridge' missing in gateway", - "polarity": "fail", - "normalized_id": "c2a.provider.sandbox.name.telegram.bridge.missing.in.gateway", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 261, - "text": "C2b: openclaw.json contains 'telegram' channel block", - "polarity": "pass", - "normalized_id": "c2b.openclaw.json.contains.telegram.channel.block", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 265, - "text": "C2b: could not read openclaw.json inside sandbox", - "polarity": "fail", - "normalized_id": "c2b.could.not.read.openclaw.json.inside.sandbox", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 267, - "text": "C2b: openclaw.json missing 'telegram' channel before stop (precondition failed)", - "polarity": "fail", - "normalized_id": "c2b.openclaw.json.missing.telegram.channel.before.stop.precondition.failed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 273, - "text": "C2c: registry.messagingChannels contains telegram (${baseline_messaging})", - "polarity": "pass", - "normalized_id": "c2c.registry.messagingchannels.contains.telegram.baseline.messaging", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 275, - "text": "C2c: registry.messagingChannels missing telegram (got: ${baseline_messaging})", - "polarity": "fail", - "normalized_id": "c2c.registry.messagingchannels.missing.telegram.got.baseline.messaging", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 280, - "text": "C2d: registry.disabledChannels empty at baseline", - "polarity": "pass", - "normalized_id": "c2d.registry.disabledchannels.empty.at.baseline", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 281, - "text": "C2d: registry.disabledChannels unexpectedly non-empty at baseline (got: ${baseline_disabled})", - "polarity": "fail", - "normalized_id": "c2d.registry.disabledchannels.unexpectedly.non.empty.at.baseline.got.baseline.disabled", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 296, - "text": "C3a: channels stop telegram registered the change", - "polarity": "pass", - "normalized_id": "c3a.channels.stop.telegram.registered.the.change", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 298, - "text": "C3a: channels stop telegram did not register", - "polarity": "fail", - "normalized_id": "c3a.channels.stop.telegram.did.not.register", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 304, - "text": "C3b: rebuild (post-stop) completed", - "polarity": "pass", - "normalized_id": "c3b.rebuild.post.stop.completed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 306, - "text": "C3b: rebuild (post-stop) failed", - "polarity": "fail", - "normalized_id": "c3b.rebuild.post.stop.failed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 321, - "text": "C4a: REGRESSION — openclaw.json still contains 'telegram' after stop+rebuild (#3453)", - "polarity": "fail", - "normalized_id": "c4a.regression.openclaw.json.still.contains.telegram.after.stop.rebuild.3453", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 327, - "text": "C4a: could not read openclaw.json inside sandbox post-stop", - "polarity": "fail", - "normalized_id": "c4a.could.not.read.openclaw.json.inside.sandbox.post.stop", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 329, - "text": "C4a: openclaw.json excludes 'telegram' after stop+rebuild (#3453 fixed)", - "polarity": "pass", - "normalized_id": "c4a.openclaw.json.excludes.telegram.after.stop.rebuild.3453.fixed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 337, - "text": "C4b: registry.messagingChannels still contains telegram (${post_stop_messaging})", - "polarity": "pass", - "normalized_id": "c4b.registry.messagingchannels.still.contains.telegram.post.stop.messaging", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 339, - "text": "C4b: registry.messagingChannels lost telegram after stop (got: ${post_stop_messaging})", - "polarity": "fail", - "normalized_id": "c4b.registry.messagingchannels.lost.telegram.after.stop.got.post.stop.messaging", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 345, - "text": "C4c: registry.disabledChannels contains telegram (${post_stop_disabled})", - "polarity": "pass", - "normalized_id": "c4c.registry.disabledchannels.contains.telegram.post.stop.disabled", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 347, - "text": "C4c: registry.disabledChannels missing telegram (got: ${post_stop_disabled})", - "polarity": "fail", - "normalized_id": "c4c.registry.disabledchannels.missing.telegram.got.post.stop.disabled", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 356, - "text": "C4d: telegram-bridge provider not attached to rebuilt sandbox", - "polarity": "pass", - "normalized_id": "c4d.telegram.bridge.provider.not.attached.to.rebuilt.sandbox", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 358, - "text": "C4d: telegram-bridge provider still attached after stop+rebuild (${attached})", - "polarity": "fail", - "normalized_id": "c4d.telegram.bridge.provider.still.attached.after.stop.rebuild.attached", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 373, - "text": "C5a: channels start telegram registered the change", - "polarity": "pass", - "normalized_id": "c5a.channels.start.telegram.registered.the.change", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 375, - "text": "C5a: channels start telegram did not register", - "polarity": "fail", - "normalized_id": "c5a.channels.start.telegram.did.not.register", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 381, - "text": "C5b: rebuild (post-start) completed", - "polarity": "pass", - "normalized_id": "c5b.rebuild.post.start.completed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 383, - "text": "C5b: rebuild (post-start) failed", - "polarity": "fail", - "normalized_id": "c5b.rebuild.post.start.failed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 397, - "text": "C6a: openclaw.json contains 'telegram' again after start+rebuild (#3381 fixed)", - "polarity": "pass", - "normalized_id": "c6a.openclaw.json.contains.telegram.again.after.start.rebuild.3381.fixed", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 401, - "text": "C6a: could not read openclaw.json inside sandbox post-start", - "polarity": "fail", - "normalized_id": "c6a.could.not.read.openclaw.json.inside.sandbox.post.start", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 403, - "text": "C6a: openclaw.json missing 'telegram' after start+rebuild (#3381 regression)", - "polarity": "fail", - "normalized_id": "c6a.openclaw.json.missing.telegram.after.start.rebuild.3381.regression", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 410, - "text": "C6b: registry.disabledChannels cleared (${post_start_disabled})", - "polarity": "pass", - "normalized_id": "c6b.registry.disabledchannels.cleared.post.start.disabled", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 411, - "text": "C6b: registry.disabledChannels still set after start (got: ${post_start_disabled})", - "polarity": "fail", - "normalized_id": "c6b.registry.disabledchannels.still.set.after.start.got.post.start.disabled", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 416, - "text": "C6c: telegram-bridge provider record present in gateway (cached token reused)", - "polarity": "pass", - "normalized_id": "c6c.telegram.bridge.provider.record.present.in.gateway.cached.token.reused", - "mapping_status": "deferred" - }, - { - "script": "test/e2e/test-channels-stop-start.sh", - "line": 418, - "text": "C6c: telegram-bridge provider record missing in gateway after start", - "polarity": "fail", - "normalized_id": "c6c.telegram.bridge.provider.record.missing.in.gateway.after.start", - "mapping_status": "deferred" - } - ] + "assertions": [], + "zero_assertion_review": { + "reason": "TODO: review legacy entrypoint for assertions not expressed as PASS/FAIL output" + } }, { "script": "test/e2e/test-cloud-inference-e2e.sh", @@ -16586,7 +16220,7 @@ ], "totals": { "scripts": 52, - "assertions": 2040, - "zero_assertion_scripts": 1 + "assertions": 1994, + "zero_assertion_scripts": 2 } } diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index 750e9b77981..58b97a4ed29 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -412,239 +412,10 @@ scripts: secret_requirement: NVIDIA_API_KEY secret and network egress test-channels-stop-start.sh: scenario: ubuntu-repo-cloud-openclaw - status: not-started + status: deferred bucket: providers-messaging - assertions: - - legacy: 'C0: NVIDIA_API_KEY is required' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C0: NVIDIA_API_KEY is set' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C0: NEMOCLAW_NON_INTERACTIVE=1 is required' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C0: NEMOCLAW_NON_INTERACTIVE=1 is set' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1a: Pre-cleanup complete' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1b: install.sh + onboard completed (exit 0)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1b: install.sh failed (exit $install_exit)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1c: openshell not on PATH after install' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1c: openshell installed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1d: nemoclaw not on PATH after install' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1d: nemoclaw installed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1e: Sandbox ''${SANDBOX_NAME}'' is Ready' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C1e: Sandbox ''${SANDBOX_NAME}'' not Ready' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2a: Provider ''${SANDBOX_NAME}-telegram-bridge'' exists in gateway' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2a: Provider ''${SANDBOX_NAME}-telegram-bridge'' missing in gateway' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2b: openclaw.json contains ''telegram'' channel block' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2b: could not read openclaw.json inside sandbox' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2b: openclaw.json missing ''telegram'' channel before stop (precondition failed)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2c: registry.messagingChannels contains telegram (${baseline_messaging})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2c: registry.messagingChannels missing telegram (got: ${baseline_messaging})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2d: registry.disabledChannels empty at baseline' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C2d: registry.disabledChannels unexpectedly non-empty at baseline (got: ${baseline_disabled})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C3a: channels stop telegram registered the change' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C3a: channels stop telegram did not register' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C3b: rebuild (post-stop) completed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C3b: rebuild (post-stop) failed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4a: REGRESSION — openclaw.json still contains ''telegram'' after stop+rebuild (#3453)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4a: could not read openclaw.json inside sandbox post-stop' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4a: openclaw.json excludes ''telegram'' after stop+rebuild (#3453 fixed)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4b: registry.messagingChannels still contains telegram (${post_stop_messaging})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4b: registry.messagingChannels lost telegram after stop (got: ${post_stop_messaging})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4c: registry.disabledChannels contains telegram (${post_stop_disabled})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4c: registry.disabledChannels missing telegram (got: ${post_stop_disabled})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4d: telegram-bridge provider not attached to rebuilt sandbox' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C4d: telegram-bridge provider still attached after stop+rebuild (${attached})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C5a: channels start telegram registered the change' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C5a: channels start telegram did not register' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C5b: rebuild (post-start) completed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C5b: rebuild (post-start) failed' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6a: openclaw.json contains ''telegram'' again after start+rebuild (#3381 fixed)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6a: could not read openclaw.json inside sandbox post-start' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6a: openclaw.json missing ''telegram'' after start+rebuild (#3381 regression)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6b: registry.disabledChannels cleared (${post_start_disabled})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6b: registry.disabledChannels still set after start (got: ${post_start_disabled})' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6c: telegram-bridge provider record present in gateway (cached token reused)' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY - - legacy: 'C6c: telegram-bridge provider record missing in gateway after start' - status: deferred - reason: new regression test (issue #3462 Test 1); pending scenario-framework migration - owner: e2e-maintainers - runner_requirement: sandbox runner with NemoClaw/OpenShell CLIs and NVIDIA_API_KEY + zero_assertion_review: dynamic PASS/FAIL assertions cover OpenClaw and Hermes across telegram, discord, wechat, and slack; pending scenario-framework migration + assertions: [] test-credential-migration.sh: scenario: ubuntu-repo-cloud-openclaw status: migrated diff --git a/test/e2e/test-channels-stop-start.sh b/test/e2e/test-channels-stop-start.sh index 33002284dab..277da6321f9 100755 --- a/test/e2e/test-channels-stop-start.sh +++ b/test/e2e/test-channels-stop-start.sh @@ -2,23 +2,28 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Channel stop/start lifecycle E2E test. +# Channel stop/start/remove lifecycle E2E test. # -# Covers Test 1 from issue #3462 ("onboard telegram → channels stop → channels start"). -# Regression coverage for: -# - #3453 — `channels stop ` + rebuild must actually remove the channel -# from openclaw.json (registry `disabledChannels` was lost across -# the destroy/recreate window before the session-stash fix). -# - #3381 — `channels start ` + rebuild must re-attach the bridge from -# cached credentials without re-prompting. +# Covers Test 1 from issue #3462 ("onboard telegram -> channels stop -> channels start") +# plus the live channel removal path from issue #3671. The regression surface +# is intentionally exercised for both supported agents (OpenClaw and Hermes) +# and every messaging channel (telegram, discord, wechat, slack). # -# Telegram-only — Discord/Slack carry the same code path; this script covers -# the regression with the minimal channel surface. +# Regression coverage: +# - #3453: `channels stop ` + rebuild must actually remove the channel +# from the baked agent config while preserving cached credentials. +# - #3381: `channels start ` + rebuild must reattach cached providers +# without re-prompting. +# - #3671: `channels remove ` on a live sandbox must detach before +# deleting provider records, clear registry channel/hash state, +# un-apply the matching channel policy preset, and rebuild cleanly +# even when the original token env vars are still present. # # Prerequisites: # - Docker running -# - NVIDIA_API_KEY set (real key or fake OpenAI endpoint) -# - NEMOCLAW_NON_INTERACTIVE=1, NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 +# - NVIDIA_API_KEY set +# - NEMOCLAW_NON_INTERACTIVE=1 +# - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 # # Usage: # NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ @@ -26,7 +31,7 @@ set -uo pipefail -export NEMOCLAW_E2E_DEFAULT_TIMEOUT=2400 +export NEMOCLAW_E2E_DEFAULT_TIMEOUT="${NEMOCLAW_E2E_DEFAULT_TIMEOUT:-7200}" SCRIPT_DIR_TIMEOUT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" # shellcheck source=test/e2e/e2e-timeout.sh source "${SCRIPT_DIR_TIMEOUT}/e2e-timeout.sh" @@ -56,6 +61,16 @@ section() { printf '\033[1;36m=== %s ===\033[0m\n' "$1" } info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } +pass_msg() { + ((PASS++)) + ((TOTAL++)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail_msg() { + ((FAIL++)) + ((TOTAL++)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} print_summary() { section "Summary" @@ -73,7 +88,6 @@ print_summary() { fi } -# Repo root resolution mirrors test-token-rotation.sh. if [ -d /workspace ] && [ -f /workspace/install.sh ]; then REPO="/workspace" elif [ -f "$(cd "$(dirname "$0")/../.." && pwd)/install.sh" ]; then @@ -83,31 +97,75 @@ else exit 1 fi -SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-channels-stop-start}" +BASE_SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-channels-stop-start}" +OPENCLAW_SANDBOX_NAME="${NEMOCLAW_CHANNELS_OPENCLAW_SANDBOX_NAME:-${BASE_SANDBOX_NAME}-openclaw}" +HERMES_SANDBOX_NAME="${NEMOCLAW_CHANNELS_HERMES_SANDBOX_NAME:-${BASE_SANDBOX_NAME}-hermes}" REGISTRY="$HOME/.nemoclaw/sandboxes.json" -INSTALL_LOG="/tmp/nemoclaw-e2e-install.log" -TELEGRAM_TOKEN="${TELEGRAM_BOT_TOKEN:-test-fake-telegram-token-stop-start-e2e}" -TELEGRAM_IDS="${TELEGRAM_ALLOWED_IDS:-123456789}" +OPENSHELL_BIN="${NEMOCLAW_OPENSHELL_BIN:-openshell}" +CHANNELS=(telegram discord wechat slack) + +ACTIVE_AGENT="" +ACTIVE_SANDBOX="" + +ORIG_TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" +ORIG_TELEGRAM_ALLOWED_IDS="${TELEGRAM_ALLOWED_IDS:-}" +ORIG_TELEGRAM_REQUIRE_MENTION="${TELEGRAM_REQUIRE_MENTION:-}" +ORIG_DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN:-}" +ORIG_DISCORD_SERVER_ID="${DISCORD_SERVER_ID:-}" +ORIG_DISCORD_SERVER_IDS="${DISCORD_SERVER_IDS:-}" +ORIG_DISCORD_USER_ID="${DISCORD_USER_ID:-}" +ORIG_DISCORD_ALLOWED_IDS="${DISCORD_ALLOWED_IDS:-}" +ORIG_DISCORD_REQUIRE_MENTION="${DISCORD_REQUIRE_MENTION:-}" +ORIG_SLACK_BOT_TOKEN="${SLACK_BOT_TOKEN:-}" +ORIG_SLACK_APP_TOKEN="${SLACK_APP_TOKEN:-}" +ORIG_SLACK_ALLOWED_USERS="${SLACK_ALLOWED_USERS:-}" +ORIG_WECHAT_BOT_TOKEN="${WECHAT_BOT_TOKEN:-}" +ORIG_WECHAT_ACCOUNT_ID="${WECHAT_ACCOUNT_ID:-}" +ORIG_WECHAT_BASE_URL="${WECHAT_BASE_URL:-}" +ORIG_WECHAT_USER_ID="${WECHAT_USER_ID:-}" +ORIG_WECHAT_ALLOWED_IDS="${WECHAT_ALLOWED_IDS:-}" + +openshell() { + if [ "$OPENSHELL_BIN" = "openshell" ]; then + command openshell "$@" + else + "$OPENSHELL_BIN" "$@" + fi +} # shellcheck source=test/e2e/lib/sandbox-teardown.sh . "$(dirname "${BASH_SOURCE[0]}")/lib/sandbox-teardown.sh" -register_sandbox_for_teardown "$SANDBOX_NAME" +register_sandbox_for_teardown "$OPENCLAW_SANDBOX_NAME" +register_sandbox_for_teardown "$HERMES_SANDBOX_NAME" + +refresh_path() { + if [ -f "$HOME/.bashrc" ]; then + # shellcheck source=/dev/null + source "$HOME/.bashrc" 2>/dev/null || true + fi + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck source=/dev/null + . "$NVM_DIR/nvm.sh" + fi + if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" + fi +} -# ── sandbox_exec: capture a command's output from inside the sandbox ── -# Same pattern as test-messaging-providers.sh. sandbox_exec() { local cmd="$1" local ssh_config ssh_config="$(mktemp)" - openshell sandbox ssh-config "$SANDBOX_NAME" >"$ssh_config" 2>/dev/null + openshell sandbox ssh-config "$ACTIVE_SANDBOX" >"$ssh_config" 2>/dev/null local result - result=$(timeout 60 ssh -F "$ssh_config" \ + result=$(run_with_timeout 60 ssh -F "$ssh_config" \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=10 \ -o LogLevel=ERROR \ - "openshell-${SANDBOX_NAME}" \ + "openshell-${ACTIVE_SANDBOX}" \ "$cmd" \ 2>&1) || true @@ -115,307 +173,564 @@ sandbox_exec() { echo "$result" } -# Inspect the registry for one sandbox. Echoes a JSON blob; callers `jq` it. -# Falls back to `node -e` when jq is unavailable on the host. registry_field() { local field="$1" + if [ ! -f "$REGISTRY" ]; then + echo "null" + return + fi if command -v jq >/dev/null 2>&1; then - jq -c --arg name "$SANDBOX_NAME" --arg field "$field" \ + jq -c --arg name "$ACTIVE_SANDBOX" --arg field "$field" \ '.sandboxes[$name][$field]' "$REGISTRY" 2>/dev/null || echo "null" else node -e " const r = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); const v = (r.sandboxes || {})[process.argv[2]]?.[process.argv[3]]; process.stdout.write(JSON.stringify(v ?? null)); -" "$REGISTRY" "$SANDBOX_NAME" "$field" 2>/dev/null || echo "null" +" "$REGISTRY" "$ACTIVE_SANDBOX" "$field" 2>/dev/null || echo "null" fi } -openclaw_has_telegram() { - # Read /sandbox/.openclaw/openclaw.json from inside the sandbox and check - # for `channels.telegram`. Exit 0 if present, 1 if absent, 2 if the file - # could not be read. +registry_array_contains() { + local field="$1" + local item="$2" + local value + value="$(registry_field "$field")" + printf '%s' "$value" | grep -Fq "\"${item}\"" +} + +registry_object_has_key() { + local field="$1" + local key="$2" + local value + value="$(registry_field "$field")" + printf '%s' "$value" | grep -Fq "\"${key}\"" +} + +provider_names_for_channel() { + local sandbox="$1" + local channel="$2" + case "$channel" in + telegram) printf '%s\n' "${sandbox}-telegram-bridge" ;; + discord) printf '%s\n' "${sandbox}-discord-bridge" ;; + wechat) printf '%s\n' "${sandbox}-wechat-bridge" ;; + slack) + printf '%s\n' "${sandbox}-slack-bridge" + printf '%s\n' "${sandbox}-slack-app" + ;; + esac +} + +token_keys_for_channel() { + local channel="$1" + case "$channel" in + telegram) printf '%s\n' "TELEGRAM_BOT_TOKEN" ;; + discord) printf '%s\n' "DISCORD_BOT_TOKEN" ;; + wechat) printf '%s\n' "WECHAT_BOT_TOKEN" ;; + slack) + printf '%s\n' "SLACK_BOT_TOKEN" + printf '%s\n' "SLACK_APP_TOKEN" + ;; + esac +} + +channel_presence() { + local channel="$1" + local config_channel="$channel" local out - out=$(sandbox_exec \ - "python3 -c 'import json,sys; d=json.load(open(\"/sandbox/.openclaw/openclaw.json\")); print(\"yes\" if \"telegram\" in d.get(\"channels\",{}) else \"no\")' 2>&1") || true + if [ "$ACTIVE_AGENT" = "openclaw" ]; then + # NemoClaw's wechat channel maps to OpenClaw's upstream plugin key. + if [ "$channel" = "wechat" ]; then + config_channel="openclaw-weixin" + fi + out=$(sandbox_exec "python3 -c 'import json,sys; d=json.load(open(\"/sandbox/.openclaw/openclaw.json\")); print(\"yes\" if sys.argv[1] in d.get(\"channels\", {}) else \"no\")' '$config_channel'" | tail -1) || true + else + local probe + case "$channel" in + telegram) + probe='grep -Eq "^TELEGRAM_BOT_TOKEN=openshell:resolve:env:TELEGRAM_BOT_TOKEN$" /sandbox/.hermes/.env' + ;; + discord) + probe='grep -Eq "^DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN$" /sandbox/.hermes/.env' + ;; + wechat) + probe='grep -Eq "^WEIXIN_TOKEN=openshell:resolve:env:WECHAT_BOT_TOKEN$" /sandbox/.hermes/.env' + ;; + slack) + probe='grep -Eq "^SLACK_BOT_TOKEN=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN$" /sandbox/.hermes/.env && grep -Eq "^SLACK_APP_TOKEN=xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN$" /sandbox/.hermes/.env' + ;; + esac + out=$(sandbox_exec "if [ -r /sandbox/.hermes/.env ]; then if ${probe}; then echo yes; else echo no; fi; else echo missing; fi" | tail -1) || true + fi + case "$out" in - *yes*) return 0 ;; - *no*) return 1 ;; - *) return 2 ;; + yes) echo "yes" ;; + no) echo "no" ;; + *) echo "error:${out}" ;; esac } -# ══════════════════════════════════════════════════════════════════ -# Phase 0: Prerequisites -# ══════════════════════════════════════════════════════════════════ -section "Phase 0: Prerequisites" +dump_channel_state() { + info "registry.messagingChannels: $(registry_field messagingChannels)" + info "registry.disabledChannels: $(registry_field disabledChannels)" + info "registry.providerCredentialHashes: $(registry_field providerCredentialHashes)" + if [ "$ACTIVE_AGENT" = "openclaw" ]; then + info "openclaw.json channels:" + sandbox_exec "python3 -c 'import json; print(list(json.load(open(\"/sandbox/.openclaw/openclaw.json\")).get(\"channels\", {}).keys()))' 2>&1" | head -10 || true + else + info ".hermes/.env messaging keys:" + sandbox_exec "grep -E '^(TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN|WEIXIN_TOKEN)=' /sandbox/.hermes/.env 2>/dev/null || true" | head -20 || true + fi +} -if [ -z "${NVIDIA_API_KEY:-}" ]; then - fail "C0: NVIDIA_API_KEY is required" - print_summary -fi -pass "C0: NVIDIA_API_KEY is set" +assert_all_config_channels() { + local expected="$1" + local context="$2" + local channel status msg + for channel in "${CHANNELS[@]}"; do + status="$(channel_presence "$channel")" + if [ "$expected" = "present" ] && [ "$status" = "yes" ]; then + msg="${ACTIVE_AGENT}/${channel}: agent config contains channel ${context}" + pass_msg "$msg" + elif [ "$expected" = "absent" ] && [ "$status" = "no" ]; then + msg="${ACTIVE_AGENT}/${channel}: agent config excludes channel ${context}" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: expected channel ${expected} in agent config ${context}, got ${status}" + fail_msg "$msg" + dump_channel_state + fi + done +} -if [ "${NEMOCLAW_NON_INTERACTIVE:-}" != "1" ]; then - fail "C0: NEMOCLAW_NON_INTERACTIVE=1 is required" - print_summary -fi -pass "C0: NEMOCLAW_NON_INTERACTIVE=1 is set" +assert_registry_channels() { + local expected="$1" + local context="$2" + local channel msg + for channel in "${CHANNELS[@]}"; do + if [ "$expected" = "present" ] && registry_array_contains messagingChannels "$channel"; then + msg="${ACTIVE_AGENT}/${channel}: registry.messagingChannels contains channel ${context}" + pass_msg "$msg" + elif [ "$expected" = "absent" ] && ! registry_array_contains messagingChannels "$channel"; then + msg="${ACTIVE_AGENT}/${channel}: registry.messagingChannels excludes channel ${context}" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: registry.messagingChannels expected ${expected} ${context}, got $(registry_field messagingChannels)" + fail_msg "$msg" + fi + done +} -# ══════════════════════════════════════════════════════════════════ -# Phase 1: Install + onboard with Telegram enabled -# ══════════════════════════════════════════════════════════════════ -section "Phase 1: Install + onboard sandbox with Telegram" +assert_disabled_channels() { + local expected="$1" + local context="$2" + local channel msg value + value="$(registry_field disabledChannels)" + for channel in "${CHANNELS[@]}"; do + if [ "$expected" = "present" ] && registry_array_contains disabledChannels "$channel"; then + msg="${ACTIVE_AGENT}/${channel}: registry.disabledChannels contains channel ${context}" + pass_msg "$msg" + elif [ "$expected" = "absent" ] && ! registry_array_contains disabledChannels "$channel"; then + msg="${ACTIVE_AGENT}/${channel}: registry.disabledChannels excludes channel ${context}" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: registry.disabledChannels expected ${expected} ${context}, got ${value}" + fail_msg "$msg" + fi + done +} -cd "$REPO" || exit 1 +assert_provider_records_exist() { + local context="$1" + local channel provider msg + for channel in "${CHANNELS[@]}"; do + while IFS= read -r provider; do + if openshell provider get "$provider" >/dev/null 2>&1; then + msg="${ACTIVE_AGENT}/${provider}: provider record exists ${context}" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${provider}: provider record missing ${context}" + fail_msg "$msg" + fi + done < <(provider_names_for_channel "$ACTIVE_SANDBOX" "$channel") + done +} -# Pre-cleanup: leftover sandboxes from prior runs. -info "Pre-cleanup..." -if command -v nemoclaw >/dev/null 2>&1; then - nemoclaw "$SANDBOX_NAME" destroy --yes 2>/dev/null || true -fi -if openshell --version >/dev/null 2>&1; then - openshell sandbox delete "$SANDBOX_NAME" 2>/dev/null || true - openshell gateway destroy -g nemoclaw 2>/dev/null || true -fi -pass "C1a: Pre-cleanup complete" - -# Skip the host-side Telegram reachability probe in onboard — the fake token -# would fail Bot API contact anyway. -if [ -z "${NEMOCLAW_SKIP_TELEGRAM_REACHABILITY:-}" ]; then - if ! curl -fsS --max-time 10 https://api.telegram.org/ >/dev/null 2>&1; then - export NEMOCLAW_SKIP_TELEGRAM_REACHABILITY=1 - info "api.telegram.org unreachable from host; setting NEMOCLAW_SKIP_TELEGRAM_REACHABILITY=1" - fi -fi +assert_channel_providers_deleted() { + local channel="$1" + local context="$2" + local provider msg + while IFS= read -r provider; do + if openshell provider get "$provider" >/dev/null 2>&1; then + msg="${ACTIVE_AGENT}/${provider}: provider record still exists ${context}" + fail_msg "$msg" + else + msg="${ACTIVE_AGENT}/${provider}: provider record deleted ${context}" + pass_msg "$msg" + fi + done < <(provider_names_for_channel "$ACTIVE_SANDBOX" "$channel") +} -export NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" -export NEMOCLAW_RECREATE_SANDBOX=1 -export NEMOCLAW_FRESH=1 -export TELEGRAM_BOT_TOKEN="$TELEGRAM_TOKEN" -export TELEGRAM_ALLOWED_IDS="$TELEGRAM_IDS" - -info "Running install.sh --non-interactive (this takes 5-10 min on first run)..." -bash install.sh --non-interactive >"$INSTALL_LOG" 2>&1 & -install_pid=$! -tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null & -tail_pid=$! -wait $install_pid -install_exit=$? -kill $tail_pid 2>/dev/null || true -wait $tail_pid 2>/dev/null || true - -# Refresh PATH for nvm-managed installs. -if [ -f "$HOME/.bashrc" ]; then - # shellcheck source=/dev/null - source "$HOME/.bashrc" 2>/dev/null || true -fi -export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" -if [ -s "$NVM_DIR/nvm.sh" ]; then - # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" -fi -if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - export PATH="$HOME/.local/bin:$PATH" -fi +assert_channel_hashes_absent() { + local channel="$1" + local context="$2" + local key msg + while IFS= read -r key; do + if registry_object_has_key providerCredentialHashes "$key"; then + msg="${ACTIVE_AGENT}/${channel}: registry.providerCredentialHashes still contains ${key} ${context}" + fail_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: registry.providerCredentialHashes excludes ${key} ${context}" + pass_msg "$msg" + fi + done < <(token_keys_for_channel "$channel") +} -if [ $install_exit -eq 0 ]; then - pass "C1b: install.sh + onboard completed (exit 0)" -else - fail "C1b: install.sh failed (exit $install_exit)" - tail -30 "$INSTALL_LOG" 2>/dev/null || true - print_summary -fi +assert_policy_preset_active() { + local channel="$1" + local expected="$2" + local context="$3" + local log="/tmp/nc-channels-${ACTIVE_AGENT}-policy-list-${channel}.log" + local msg + if ! nemoclaw "$ACTIVE_SANDBOX" policy-list >"$log" 2>&1; then + msg="${ACTIVE_AGENT}/${channel}: policy-list failed ${context}" + fail_msg "$msg" + tail -30 "$log" 2>/dev/null || true + return + fi -if ! openshell --version >/dev/null 2>&1; then - fail "C1c: openshell not on PATH after install" - print_summary -fi -pass "C1c: openshell installed" + if [ "$expected" = "active" ]; then + if grep -q "● ${channel}" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channel policy preset active ${context}" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: channel policy preset not active ${context}" + fail_msg "$msg" + grep -F "$channel" "$log" | head -5 || true + fi + else + if grep -q "● ${channel}" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channel policy preset still active ${context}" + fail_msg "$msg" + grep -F "$channel" "$log" | head -5 || true + else + msg="${ACTIVE_AGENT}/${channel}: channel policy preset inactive ${context}" + pass_msg "$msg" + fi + fi +} -if ! command -v nemoclaw >/dev/null 2>&1; then - fail "C1d: nemoclaw not on PATH after install" - print_summary -fi -pass "C1d: nemoclaw installed" +export_fake_channel_env() { + local suffix="$1" + export TELEGRAM_BOT_TOKEN="${ORIG_TELEGRAM_BOT_TOKEN:-test-fake-telegram-token-${suffix}}" + export TELEGRAM_ALLOWED_IDS="${ORIG_TELEGRAM_ALLOWED_IDS:-123456789,987654321}" + export TELEGRAM_REQUIRE_MENTION="${ORIG_TELEGRAM_REQUIRE_MENTION:-0}" + + export DISCORD_BOT_TOKEN="${ORIG_DISCORD_BOT_TOKEN:-test-fake-discord-token-${suffix}}" + export DISCORD_SERVER_ID="${ORIG_DISCORD_SERVER_ID:-1491590992753590594}" + export DISCORD_SERVER_IDS="${ORIG_DISCORD_SERVER_IDS:-${DISCORD_SERVER_ID}}" + export DISCORD_USER_ID="${ORIG_DISCORD_USER_ID:-1005536447329222676}" + export DISCORD_ALLOWED_IDS="${ORIG_DISCORD_ALLOWED_IDS:-${DISCORD_USER_ID}}" + export DISCORD_REQUIRE_MENTION="${ORIG_DISCORD_REQUIRE_MENTION:-0}" + + export SLACK_BOT_TOKEN="${ORIG_SLACK_BOT_TOKEN:-xoxb-fake-slack-token-${suffix}}" + export SLACK_APP_TOKEN="${ORIG_SLACK_APP_TOKEN:-xapp-fake-slack-app-token-${suffix}}" + export SLACK_ALLOWED_USERS="${ORIG_SLACK_ALLOWED_USERS:-U0123456789,U09ABCDEFGH}" + + export WECHAT_BOT_TOKEN="${ORIG_WECHAT_BOT_TOKEN:-test-fake-wechat-token-${suffix}}" + export WECHAT_ACCOUNT_ID="${ORIG_WECHAT_ACCOUNT_ID:-e2e-fake-account-${suffix}}" + export WECHAT_BASE_URL="${ORIG_WECHAT_BASE_URL:-https://ilinkai-fake-${suffix}.wechat.com}" + export WECHAT_USER_ID="${ORIG_WECHAT_USER_ID:-wxid_${suffix}_operator}" + export WECHAT_ALLOWED_IDS="${ORIG_WECHAT_ALLOWED_IDS:-${WECHAT_USER_ID}}" +} -if openshell sandbox list 2>&1 | grep -q "${SANDBOX_NAME}.*Ready"; then - pass "C1e: Sandbox '${SANDBOX_NAME}' is Ready" -else - fail "C1e: Sandbox '${SANDBOX_NAME}' not Ready" - print_summary -fi +pre_cleanup_sandbox() { + local sandbox="$1" + info "Pre-cleanup for ${sandbox}..." + if command -v nemoclaw >/dev/null 2>&1; then + nemoclaw "$sandbox" destroy --yes 2>/dev/null || true + fi + if openshell --version >/dev/null 2>&1; then + openshell sandbox delete "$sandbox" 2>/dev/null || true + local channel provider + for channel in "${CHANNELS[@]}"; do + while IFS= read -r provider; do + openshell provider delete "$provider" 2>/dev/null || true + done < <(provider_names_for_channel "$sandbox" "$channel") + done + openshell gateway destroy -g nemoclaw 2>/dev/null || true + fi +} -# ══════════════════════════════════════════════════════════════════ -# Phase 2: Verify baseline state (Telegram active) -# ══════════════════════════════════════════════════════════════════ -section "Phase 2: Verify baseline state (Telegram active)" +install_for_active_agent() { + local log="/tmp/nemoclaw-e2e-channels-${ACTIVE_AGENT}-install.log" + export NEMOCLAW_SANDBOX_NAME="$ACTIVE_SANDBOX" + export NEMOCLAW_AGENT="$ACTIVE_AGENT" + export NEMOCLAW_POLICY_TIER="${NEMOCLAW_POLICY_TIER:-open}" + export NEMOCLAW_RECREATE_SANDBOX=1 + export NEMOCLAW_FRESH=1 + + if [ -z "${NEMOCLAW_SKIP_TELEGRAM_REACHABILITY:-}" ]; then + if ! curl -fsS --max-time 10 https://api.telegram.org/ >/dev/null 2>&1; then + export NEMOCLAW_SKIP_TELEGRAM_REACHABILITY=1 + info "api.telegram.org unreachable from host; setting NEMOCLAW_SKIP_TELEGRAM_REACHABILITY=1" + fi + fi -if openshell provider get "${SANDBOX_NAME}-telegram-bridge" >/dev/null 2>&1; then - pass "C2a: Provider '${SANDBOX_NAME}-telegram-bridge' exists in gateway" -else - fail "C2a: Provider '${SANDBOX_NAME}-telegram-bridge' missing in gateway" -fi + info "Running install.sh --non-interactive for ${ACTIVE_AGENT} (${ACTIVE_SANDBOX})..." + bash install.sh --non-interactive >"$log" 2>&1 & + local install_pid=$! + tail -f "$log" --pid=$install_pid 2>/dev/null & + local tail_pid=$! + wait $install_pid + local install_exit=$? + kill $tail_pid 2>/dev/null || true + wait $tail_pid 2>/dev/null || true + cp "$log" /tmp/nemoclaw-e2e-install.log 2>/dev/null || true + + refresh_path + + local msg + if [ "$install_exit" -eq 0 ]; then + msg="${ACTIVE_AGENT}: install.sh + onboard completed" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}: install.sh failed with exit ${install_exit}" + fail_msg "$msg" + tail -40 "$log" 2>/dev/null || true + print_summary + fi +} -if openclaw_has_telegram; then - pass "C2b: openclaw.json contains 'telegram' channel block" -else - rc=$? - if [ "$rc" = "2" ]; then - fail "C2b: could not read openclaw.json inside sandbox" +run_rebuild() { + local phase="$1" + local log="/tmp/nc-channels-${ACTIVE_AGENT}-rebuild-${phase}.log" + local msg + info "Rebuilding ${ACTIVE_SANDBOX} for ${phase}..." + if nemoclaw "$ACTIVE_SANDBOX" rebuild --yes >"$log" 2>&1; then + msg="${ACTIVE_AGENT}: rebuild completed after ${phase}" + pass_msg "$msg" else - fail "C2b: openclaw.json missing 'telegram' channel before stop (precondition failed)" + msg="${ACTIVE_AGENT}: rebuild failed after ${phase}" + fail_msg "$msg" + tail -40 "$log" 2>/dev/null || true + dump_channel_state + print_summary fi -fi +} -baseline_messaging=$(registry_field messagingChannels) -if echo "$baseline_messaging" | grep -q '"telegram"'; then - pass "C2c: registry.messagingChannels contains telegram (${baseline_messaging})" -else - fail "C2c: registry.messagingChannels missing telegram (got: ${baseline_messaging})" -fi +stop_all_channels() { + local channel log rc msg + for channel in "${CHANNELS[@]}"; do + log="/tmp/nc-channels-${ACTIVE_AGENT}-stop-${channel}.log" + if nemoclaw "$ACTIVE_SANDBOX" channels stop "$channel" >"$log" 2>&1; then + rc=0 + else + rc=$? + fi + cat "$log" + if [ "$rc" -eq 0 ] && grep -q "Marked ${channel} disabled" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channels stop registered" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: channels stop failed or did not register" + fail_msg "$msg" + tail -20 "$log" 2>/dev/null || true + fi + done +} -baseline_disabled=$(registry_field disabledChannels) -case "$baseline_disabled" in - "null" | "[]") pass "C2d: registry.disabledChannels empty at baseline" ;; - *) fail "C2d: registry.disabledChannels unexpectedly non-empty at baseline (got: ${baseline_disabled})" ;; -esac +start_all_channels() { + local channel log rc msg + for channel in "${CHANNELS[@]}"; do + log="/tmp/nc-channels-${ACTIVE_AGENT}-start-${channel}.log" + if nemoclaw "$ACTIVE_SANDBOX" channels start "$channel" >"$log" 2>&1; then + rc=0 + else + rc=$? + fi + cat "$log" + if [ "$rc" -eq 0 ] && grep -q "Marked ${channel} enabled" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channels start registered" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: channels start failed or did not register" + fail_msg "$msg" + tail -20 "$log" 2>/dev/null || true + fi + done +} -# ══════════════════════════════════════════════════════════════════ -# Phase 3: Stop telegram + rebuild -# ══════════════════════════════════════════════════════════════════ -section "Phase 3: channels stop telegram + rebuild" +remove_all_channels() { + local channel log rc msg + for channel in "${CHANNELS[@]}"; do + log="/tmp/nc-channels-${ACTIVE_AGENT}-remove-${channel}.log" + if nemoclaw "$ACTIVE_SANDBOX" channels remove "$channel" >"$log" 2>&1; then + rc=0 + else + rc=$? + fi + cat "$log" + if [ "$rc" -eq 0 ] && grep -q "Removed ${channel} bridge" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channels remove completed on a live sandbox" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: channels remove failed" + fail_msg "$msg" + tail -30 "$log" 2>/dev/null || true + fi + if grep -q "Change queued.*remove '${channel}'" "$log"; then + msg="${ACTIVE_AGENT}/${channel}: channels remove queued rebuild" + pass_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: channels remove did not queue rebuild" + fail_msg "$msg" + fi + + assert_channel_providers_deleted "$channel" "after channels remove" + + if registry_array_contains messagingChannels "$channel"; then + msg="${ACTIVE_AGENT}/${channel}: registry.messagingChannels still contains channel after remove" + fail_msg "$msg" + else + msg="${ACTIVE_AGENT}/${channel}: registry.messagingChannels excludes channel after remove" + pass_msg "$msg" + fi + assert_channel_hashes_absent "$channel" "after remove" + assert_policy_preset_active "$channel" "inactive" "after remove" + done +} -if nemoclaw "$SANDBOX_NAME" channels stop telegram >/tmp/nc-stop.log 2>&1; then - stop_rc=0 -else - stop_rc=$? -fi -cat /tmp/nc-stop.log -if [ "$stop_rc" -eq 0 ] && grep -q "Marked telegram" /tmp/nc-stop.log; then - pass "C3a: channels stop telegram registered the change" -else - fail "C3a: channels stop telegram did not register" - tail -20 /tmp/nc-stop.log 2>/dev/null || true -fi +destroy_completed_sandbox() { + local sandbox="$1" + info "Destroying completed sandbox ${sandbox} before the next scenario..." + if command -v nemoclaw >/dev/null 2>&1; then + nemoclaw "$sandbox" destroy --yes >/dev/null 2>&1 || true + fi + if openshell --version >/dev/null 2>&1; then + openshell gateway destroy -g nemoclaw >/dev/null 2>&1 || true + fi +} -info "Rebuilding sandbox to apply the stop..." -if nemoclaw "$SANDBOX_NAME" rebuild --yes >/tmp/nc-rebuild-stop.log 2>&1; then - pass "C3b: rebuild (post-stop) completed" -else - fail "C3b: rebuild (post-stop) failed" - tail -30 /tmp/nc-rebuild-stop.log 2>/dev/null || true - print_summary -fi +run_agent_scenario() { + local agent="$1" + local sandbox="$2" + ACTIVE_AGENT="$agent" + ACTIVE_SANDBOX="$sandbox" + export NEMOCLAW_AGENT="$ACTIVE_AGENT" + + section "Scenario: ${agent} all messaging channels" + pre_cleanup_sandbox "$ACTIVE_SANDBOX" + export_fake_channel_env "${agent}" + + cd "$REPO" || exit 1 + install_for_active_agent + + local msg + if ! openshell --version >/dev/null 2>&1; then + msg="${ACTIVE_AGENT}: openshell not on PATH after install" + fail_msg "$msg" + print_summary + fi + msg="${ACTIVE_AGENT}: openshell installed" + pass_msg "$msg" -# ══════════════════════════════════════════════════════════════════ -# Phase 4: Post-stop assertions (Test 1 acceptance criteria, #3453) -# ══════════════════════════════════════════════════════════════════ -section "Phase 4: Verify post-stop state (regression #3453)" - -# C4a: THE REGRESSION CHECK. Before the session-stash fix, the rebuild -# destroyed the registry entry before onboard --resume read disabledChannels -# back — so the filter was a no-op and telegram came back live. This is the -# load-bearing assertion of the whole test. -if openclaw_has_telegram; then - fail "C4a: REGRESSION — openclaw.json still contains 'telegram' after stop+rebuild (#3453)" - info "openclaw.json channels after stop+rebuild:" - sandbox_exec "python3 -c 'import json; print(list(json.load(open(\"/sandbox/.openclaw/openclaw.json\")).get(\"channels\",{}).keys()))' 2>&1" | head -5 -else - rc=$? - if [ "$rc" = "2" ]; then - fail "C4a: could not read openclaw.json inside sandbox post-stop" + if ! command -v nemoclaw >/dev/null 2>&1; then + msg="${ACTIVE_AGENT}: nemoclaw not on PATH after install" + fail_msg "$msg" + print_summary + fi + msg="${ACTIVE_AGENT}: nemoclaw installed" + pass_msg "$msg" + + if openshell sandbox list 2>&1 | grep -q "${ACTIVE_SANDBOX}.*Ready"; then + msg="${ACTIVE_AGENT}: sandbox ${ACTIVE_SANDBOX} is Ready" + pass_msg "$msg" else - pass "C4a: openclaw.json excludes 'telegram' after stop+rebuild (#3453 fixed)" + msg="${ACTIVE_AGENT}: sandbox ${ACTIVE_SANDBOX} is not Ready" + fail_msg "$msg" + openshell sandbox list 2>&1 || true + print_summary fi -fi -# C4b: messagingChannels keeps telegram so `channels start` can recover it -# (deliberate — the channel isn't removed, just paused). -post_stop_messaging=$(registry_field messagingChannels) -if echo "$post_stop_messaging" | grep -q '"telegram"'; then - pass "C4b: registry.messagingChannels still contains telegram (${post_stop_messaging})" -else - fail "C4b: registry.messagingChannels lost telegram after stop (got: ${post_stop_messaging})" -fi + section "${agent}: baseline with all channels active" + assert_provider_records_exist "at baseline" + assert_all_config_channels "present" "at baseline" + assert_registry_channels "present" "at baseline" + assert_disabled_channels "absent" "at baseline" + for channel in "${CHANNELS[@]}"; do + assert_policy_preset_active "$channel" "active" "at baseline" + done + + section "${agent}: channels stop all + rebuild" + stop_all_channels + run_rebuild "stop-all" + + section "${agent}: verify stopped state" + assert_all_config_channels "absent" "after stop+rebuild" + assert_registry_channels "present" "after stop" + assert_disabled_channels "present" "after stop" + assert_provider_records_exist "after stop" + + section "${agent}: channels start all + rebuild" + start_all_channels + run_rebuild "start-all" + + section "${agent}: verify restarted state" + assert_all_config_channels "present" "after start+rebuild" + assert_registry_channels "present" "after start" + assert_disabled_channels "absent" "after start" + assert_provider_records_exist "after start" + + section "${agent}: channels remove all on live sandbox" + remove_all_channels + + section "${agent}: rebuild after channels remove" + run_rebuild "remove-all" + assert_all_config_channels "absent" "after remove+rebuild" + assert_registry_channels "absent" "after remove+rebuild" + assert_disabled_channels "absent" "after remove+rebuild" +} -# C4c: disabledChannels must contain telegram. -post_stop_disabled=$(registry_field disabledChannels) -if echo "$post_stop_disabled" | grep -q '"telegram"'; then - pass "C4c: registry.disabledChannels contains telegram (${post_stop_disabled})" -else - fail "C4c: registry.disabledChannels missing telegram (got: ${post_stop_disabled})" -fi +section "Phase 0: Prerequisites" -# C4d: The bridge provider must NOT be attached to the rebuilt sandbox. The -# provider record itself stays in the gateway (so `channels start` can -# re-attach without re-prompting); only the sandbox attachment is gone. -attached=$(openshell sandbox describe "$SANDBOX_NAME" 2>&1 \ - | grep -F "${SANDBOX_NAME}-telegram-bridge" || true) -if [ -z "$attached" ]; then - pass "C4d: telegram-bridge provider not attached to rebuilt sandbox" -else - fail "C4d: telegram-bridge provider still attached after stop+rebuild (${attached})" +if [ -z "${NVIDIA_API_KEY:-}" ]; then + msg="C0: NVIDIA_API_KEY is required" + fail_msg "$msg" + print_summary fi +msg="C0: NVIDIA_API_KEY is set" +pass_msg "$msg" -# ══════════════════════════════════════════════════════════════════ -# Phase 5: Start telegram + rebuild -# ══════════════════════════════════════════════════════════════════ -section "Phase 5: channels start telegram + rebuild" - -if nemoclaw "$SANDBOX_NAME" channels start telegram >/tmp/nc-start.log 2>&1; then - start_rc=0 -else - start_rc=$? -fi -cat /tmp/nc-start.log -if [ "$start_rc" -eq 0 ] && grep -q "Marked telegram" /tmp/nc-start.log; then - pass "C5a: channels start telegram registered the change" -else - fail "C5a: channels start telegram did not register" - tail -20 /tmp/nc-start.log 2>/dev/null || true +if [ "${NEMOCLAW_NON_INTERACTIVE:-}" != "1" ]; then + msg="C0: NEMOCLAW_NON_INTERACTIVE=1 is required" + fail_msg "$msg" + print_summary fi +msg="C0: NEMOCLAW_NON_INTERACTIVE=1 is set" +pass_msg "$msg" -info "Rebuilding sandbox to apply the start..." -if nemoclaw "$SANDBOX_NAME" rebuild --yes >/tmp/nc-rebuild-start.log 2>&1; then - pass "C5b: rebuild (post-start) completed" -else - fail "C5b: rebuild (post-start) failed" - tail -30 /tmp/nc-rebuild-start.log 2>/dev/null || true +if [ "${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}" != "1" ]; then + msg="C0: NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 is required" + fail_msg "$msg" print_summary fi +msg="C0: NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 is set" +pass_msg "$msg" -# ══════════════════════════════════════════════════════════════════ -# Phase 6: Post-start assertions (Test 1 acceptance criteria, #3381) -# ══════════════════════════════════════════════════════════════════ -section "Phase 6: Verify post-start state (regression #3381)" - -# C6a: Telegram block back in openclaw.json. The host-side credential is -# still cached from Phase 1 (channels start does not re-prompt) — proves -# #3381's "start should recover from cached credentials" contract. -if openclaw_has_telegram; then - pass "C6a: openclaw.json contains 'telegram' again after start+rebuild (#3381 fixed)" +if docker info >/dev/null 2>&1; then + msg="C0: Docker is running" + pass_msg "$msg" else - rc=$? - if [ "$rc" = "2" ]; then - fail "C6a: could not read openclaw.json inside sandbox post-start" - else - fail "C6a: openclaw.json missing 'telegram' after start+rebuild (#3381 regression)" - fi + msg="C0: Docker is not running" + fail_msg "$msg" + print_summary fi -# C6b: disabledChannels cleared. -post_start_disabled=$(registry_field disabledChannels) -case "$post_start_disabled" in - "null" | "[]") pass "C6b: registry.disabledChannels cleared (${post_start_disabled})" ;; - *) fail "C6b: registry.disabledChannels still set after start (got: ${post_start_disabled})" ;; -esac +refresh_path -# C6c: Provider record still resolvable in the gateway (cached token survived). -if openshell provider get "${SANDBOX_NAME}-telegram-bridge" >/dev/null 2>&1; then - pass "C6c: telegram-bridge provider record present in gateway (cached token reused)" -else - fail "C6c: telegram-bridge provider record missing in gateway after start" -fi +run_agent_scenario "openclaw" "$OPENCLAW_SANDBOX_NAME" +destroy_completed_sandbox "$OPENCLAW_SANDBOX_NAME" +run_agent_scenario "hermes" "$HERMES_SANDBOX_NAME" print_summary