diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4b5e762e8c8..42a33385a84 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -183,6 +183,8 @@ reviews: - `cloud-e2e` — full onboard + cloud inference - `sandbox-operations-e2e` — multi-sandbox lifecycle - `rebuild-openclaw-e2e` — workspace state survives rebuild + - `channels-stop-start-e2e` — channel disable/enable lifecycle across + rebuild with cached Telegram credentials - `messaging-compatible-endpoint-e2e` — Telegram + compatible endpoint inference.local regression - `hermes-discord-e2e` — Hermes Discord config schema + placeholder @@ -193,9 +195,37 @@ reviews: To run selectively: ``` - gh workflow run nightly-e2e.yaml --ref -f jobs=cloud-e2e,sandbox-operations-e2e,rebuild-openclaw-e2e,messaging-compatible-endpoint-e2e,hermes-discord-e2e,hermes-slack-e2e,openshell-gateway-upgrade-e2e + gh workflow run nightly-e2e.yaml --ref -f jobs=cloud-e2e,sandbox-operations-e2e,rebuild-openclaw-e2e,channels-stop-start-e2e,messaging-compatible-endpoint-e2e,hermes-discord-e2e,hermes-slack-e2e,openshell-gateway-upgrade-e2e ``` + - path: "src/lib/onboard/channel-state.ts" + instructions: &e2e-channel-stop-start | + This file controls disabled channel resolution used during onboard + and rebuild. Changes affect whether `channels stop` persists across + sandbox destroy/recreate and whether `channels start` reattaches + cached credentials. + + **E2E test recommendation:** + - `channels-stop-start-e2e` — Telegram channel stop/start lifecycle + across rebuild with cached credentials + + To run selectively: + ``` + gh workflow run nightly-e2e.yaml --ref -f jobs=channels-stop-start-e2e + ``` + + - path: "src/lib/state/onboard-session.ts" + instructions: *e2e-channel-stop-start + + - path: "src/lib/actions/sandbox/rebuild.ts" + instructions: *e2e-channel-stop-start + + - path: "src/lib/commands/sandbox/channels/**" + instructions: *e2e-channel-stop-start + + - path: "test/e2e/test-channels-stop-start.sh" + instructions: *e2e-channel-stop-start + - path: "src/lib/actions/inference-set.ts" instructions: | This file switches the OpenShell inference route and patches the diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index c3cf7bf7806..1de5b525087 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -83,7 +83,7 @@ on: credential-sanitization-e2e, telegram-injection-e2e, overlayfs-autofix-e2e, device-auth-health-e2e, launchable-smoke-e2e, gpu-e2e, gpu-double-onboard-e2e, - brave-search-e2e + channels-stop-start-e2e, brave-search-e2e required: false type: string default: "" @@ -371,6 +371,45 @@ 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-e2e: + if: >- + github.repository == 'NVIDIA/NemoClaw' && + (github.event_name != 'workflow_dispatch' || + inputs.jobs == '' || + contains(format(',{0},', inputs.jobs), ',channels-stop-start-e2e,')) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.target_ref || github.ref }} + + - name: Run channels stop/start lifecycle E2E test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + NEMOCLAW_POLICY_TIER: "open" + NEMOCLAW_SANDBOX_NAME: "e2e-channels-stop-start" + GITHUB_TOKEN: ${{ github.token }} + TELEGRAM_BOT_TOKEN: "test-fake-telegram-token-stop-start-e2e" + TELEGRAM_ALLOWED_IDS: "123456789" + run: bash test/e2e/test-channels-stop-start.sh + + - name: Upload install log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: install-log-channels-stop-start + path: /tmp/nemoclaw-e2e-install.log + if-no-files-found: ignore + # ── Brave Search E2E (#2687) ───────────────────────────────── # Validates the full Brave Search path with a real BRAVE_API_KEY: # non-interactive onboard auto-enables web search, the brave network @@ -1976,6 +2015,7 @@ jobs: docs-validation-e2e, messaging-providers-e2e, messaging-compatible-endpoint-e2e, + channels-stop-start-e2e, brave-search-e2e, kimi-inference-compat-e2e, token-rotation-e2e, @@ -2068,6 +2108,7 @@ jobs: docs-validation-e2e, messaging-providers-e2e, messaging-compatible-endpoint-e2e, + channels-stop-start-e2e, brave-search-e2e, kimi-inference-compat-e2e, token-rotation-e2e, @@ -2217,6 +2258,7 @@ jobs: docs-validation-e2e, messaging-providers-e2e, messaging-compatible-endpoint-e2e, + channels-stop-start-e2e, brave-search-e2e, kimi-inference-compat-e2e, token-rotation-e2e, diff --git a/src/lib/actions/inference-set.test.ts b/src/lib/actions/inference-set.test.ts index 965f74746f9..92d8b6f278c 100644 --- a/src/lib/actions/inference-set.test.ts +++ b/src/lib/actions/inference-set.test.ts @@ -79,6 +79,7 @@ function baseSession(overrides: Partial = {}): Session { policyPresets: null, messagingChannels: null, messagingChannelConfig: null, + disabledChannels: null, migratedLegacyValueHashes: null, gpuPassthrough: false, telegramConfig: null, diff --git a/src/lib/actions/sandbox/rebuild.ts b/src/lib/actions/sandbox/rebuild.ts index 4dc1290326d..e70dff0cd2a 100644 --- a/src/lib/actions/sandbox/rebuild.ts +++ b/src/lib/actions/sandbox/rebuild.ts @@ -28,31 +28,31 @@ const { LOCAL_INFERENCE_PROVIDERS, REMOTE_PROVIDER_CONFIG } = require("../../onb REMOTE_PROVIDER_CONFIG: Record; }; -import { loadAgent } from "../../agent/defs"; -import { ensureAgentBaseImage } from "../../agent/onboard"; -import { getSandboxDeleteOutcome } from "../../domain/sandbox/destroy"; -import * as nim from "../../inference/nim"; -import type { Session } from "../../state/onboard-session"; -import * as onboardSession from "../../state/onboard-session"; -import { captureOpenshell, runOpenshell } from "../../adapters/openshell/runtime"; import { detectOpenShellStateRpcPreflightIssue, detectOpenShellStateRpcResultIssue, printOpenShellStateRpcIssue, } from "../../adapters/openshell/gateway-drift"; -import * as policies from "../../policy"; -import * as registry from "../../state/registry"; import { resolveOpenshell } from "../../adapters/openshell/resolve"; +import { captureOpenshell, runOpenshell } from "../../adapters/openshell/runtime"; +import { loadAgent } from "../../agent/defs"; +import { ensureAgentBaseImage } from "../../agent/onboard"; +import { RD as _RD, B, D, G, R, YW } from "../../cli/terminal-style"; +import { getSandboxDeleteOutcome } from "../../domain/sandbox/destroy"; +import * as nim from "../../inference/nim"; +import * as policies from "../../policy"; import { parseLiveSandboxNames } from "../../runtime-recovery"; -import { removeSandboxRegistryEntry } from "./destroy"; -import { executeSandboxCommand } from "./process-recovery"; +import * as sandboxVersion from "../../sandbox/version"; +import type { Session } from "../../state/onboard-session"; +import * as onboardSession from "../../state/onboard-session"; +import * as registry from "../../state/registry"; +import * as sandboxState from "../../state/sandbox"; import { createSystemDeps as createSessionDeps, getActiveSandboxSessions, } from "../../state/sandbox-session"; -import * as sandboxState from "../../state/sandbox"; -import * as sandboxVersion from "../../sandbox/version"; -import { B, D, G, R, RD as _RD, YW } from "../../cli/terminal-style"; +import { removeSandboxRegistryEntry } from "./destroy"; +import { executeSandboxCommand } from "./process-recovery"; const agentRuntime = require("../../../../bin/lib/agent-runtime"); @@ -543,6 +543,21 @@ export async function rebuildSandbox( sb.messagingChannelConfig ?? sessionMessagingChannelConfig ?? null; const hasRebuildMessagingChannels = registryMessagingChannels !== null || sessionMessagingChannels !== null; + // Snapshot the operator's paused channel set BEFORE `removeSandboxRegistryEntry` + // wipes the registry entry. Otherwise the `disabledChannels` filter inside + // `createSandbox` (onboard.ts) reads back `[]` from the freshly-empty registry + // and the stopped channel comes back live in the rebuilt image. The session + // mirror is the only place this list can survive the destroy/recreate window. + // + // Always re-stash from `sb` — do NOT fall back to a prior session value. + // `sb` is loaded fresh from the registry at the top of rebuildSandbox, so it + // already reflects the latest `channels stop|start` write. The session mirror + // is downstream of the registry; re-stashing on every rebuild keeps a stale + // ["telegram"] from a prior stop/rebuild cycle from leaking into the next + // start/rebuild and filtering the channel back out. + const rebuildDisabledChannels = Array.isArray(sb.disabledChannels) + ? sb.disabledChannels.filter((value: unknown): value is string => typeof value === "string") + : []; log( `Session before update: sandboxName=${sessionBefore?.sandboxName}, status=${sessionBefore?.status}, resumable=${sessionBefore?.resumable}, provider=${sessionBefore?.provider}, model=${sessionBefore?.model}, sessionMatch=${sessionMatchesSandbox}`, ); @@ -558,6 +573,7 @@ export async function rebuildSandbox( s.agent = rebuildAgent; s.messagingChannels = rebuildMessagingChannels; s.messagingChannelConfig = rebuildMessagingChannelConfig; + s.disabledChannels = rebuildDisabledChannels; // Persist inference selection from the about-to-be-removed registry entry // so onboard --resume can recreate with the same provider/model in // non-interactive mode. Without this the registry is gone by the time @@ -683,9 +699,8 @@ export async function rebuildSandbox( const preservedRegistryFields = { ...(hasRebuildMessagingChannels ? { messagingChannels: [...rebuildMessagingChannels] } : {}), - ...(Array.isArray(sb.disabledChannels) && sb.disabledChannels.length > 0 - ? { disabledChannels: [...sb.disabledChannels] } - : {}), + disabledChannels: + rebuildDisabledChannels.length > 0 ? [...rebuildDisabledChannels] : undefined, ...(sb.providerCredentialHashes ? { providerCredentialHashes: sb.providerCredentialHashes } : {}), }; if (Object.keys(preservedRegistryFields).length > 0) { diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 1311640a0ff..4bc6319562b 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -345,9 +345,9 @@ const sandboxCreateFailureDiagnostics: typeof import("./onboard/sandbox-create-f import type { AgentDefinition } from "./agent/defs"; import type { CurlProbeResult } from "./adapters/http/probe"; -import type { GatewayReuseState } from "./state/gateway"; import type { GatewayInference } from "./inference/config"; import type { GpuInfo, ValidationResult } from "./inference/local"; +import type { WebSearchConfig } from "./inference/web-search"; import { hydrateMessagingChannelConfig, type MessagingChannelConfig, @@ -355,32 +355,32 @@ import { readMessagingChannelConfigFromEnv, sanitizeMessagingChannelConfig, } from "./messaging-channel-config"; -import type { ContainerRuntime } from "./platform"; -import type { Session, SessionUpdates } from "./state/onboard-session"; +import { streamGatewayStart } from "./onboard/gateway"; +import { reportGpuPassthroughRecovery } from "./onboard/gpu-recovery"; +import { getMessagingToken } from "./onboard/messaging-token"; +import type { + DockerDriverBinaryOverrides, + OpenShellInstallDeps, + OpenShellInstallResult, +} from "./onboard/openshell-install"; +import { decidePolicyCarryForward } from "./onboard/policy-carryforward"; +import type { SelectionDrift } from "./onboard/selection-drift"; import type { ModelCatalogFetchResult, ModelValidationResult, ProbeResult, ValidationFailureLike, } from "./onboard/types"; -import { getMessagingToken } from "./onboard/messaging-token"; -import { decidePolicyCarryForward } from "./onboard/policy-carryforward"; +import type { ContainerRuntime } from "./platform"; +import type { TierDefinition, TierPreset } from "./policy/tiers"; import { channelHasStaticToken, getChannelTokenKeys, listChannels } from "./sandbox/channels"; -import { streamGatewayStart } from "./onboard/gateway"; -import { reportGpuPassthroughRecovery } from "./onboard/gpu-recovery"; import type { StreamSandboxCreateResult } from "./sandbox/create-stream"; +import type { GatewayReuseState } from "./state/gateway"; +import type { Session, SessionUpdates } from "./state/onboard-session"; import type { SandboxEntry } from "./state/registry"; import type { BackupResult } from "./state/sandbox"; -import type { TierDefinition, TierPreset } from "./policy/tiers"; import type { SandboxCreateFailure, ValidationClassification } from "./validation"; import type { ProbeRecovery } from "./validation-recovery"; -import type { WebSearchConfig } from "./inference/web-search"; -import type { - DockerDriverBinaryOverrides, - OpenShellInstallDeps, - OpenShellInstallResult, -} from "./onboard/openshell-install"; -import type { SelectionDrift } from "./onboard/selection-drift"; const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; const USE_COLOR = !process.env.NO_COLOR && !!process.stdout.isTTY; @@ -5120,7 +5120,7 @@ async function createSandbox( // Credentials stay in the keychain; the bridge simply isn't registered with // the gateway on the next rebuild. `channels start` removes the entry and // the bridge comes back. - const disabledChannels = registry.getDisabledChannels(sandboxName); + const disabledChannels = require("./onboard/channel-state").resolveDisabledChannels(sandboxName); const disabledEnvKeys = new Set( MESSAGING_CHANNELS.filter((c) => disabledChannels.includes(c.name)).flatMap((c) => getChannelTokenKeys(c), diff --git a/src/lib/onboard/channel-state.test.ts b/src/lib/onboard/channel-state.test.ts new file mode 100644 index 00000000000..ca203f38b09 --- /dev/null +++ b/src/lib/onboard/channel-state.test.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +import { resolveDisabledChannels } from "./channel-state"; + +describe("onboard channel state helpers", () => { + it("prefers disabledChannels from the onboard session mirror", () => { + const getRegistryDisabledChannels = vi.fn(() => ["discord"]); + + expect( + resolveDisabledChannels("alpha", { + loadSession: () => ({ disabledChannels: ["telegram"] }), + getRegistryDisabledChannels, + }), + ).toEqual(["telegram"]); + expect(getRegistryDisabledChannels).not.toHaveBeenCalled(); + }); + + it("falls back to the registry when the session has no mirror", () => { + expect( + resolveDisabledChannels("alpha", { + loadSession: () => ({ disabledChannels: null }), + getRegistryDisabledChannels: (sandboxName) => + sandboxName === "alpha" ? ["discord"] : [], + }), + ).toEqual(["discord"]); + }); + + it("treats an empty session mirror as authoritative", () => { + const getRegistryDisabledChannels = vi.fn(() => ["telegram"]); + + expect( + resolveDisabledChannels("alpha", { + loadSession: () => ({ disabledChannels: [] }), + getRegistryDisabledChannels, + }), + ).toEqual([]); + expect(getRegistryDisabledChannels).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/onboard/channel-state.ts b/src/lib/onboard/channel-state.ts new file mode 100644 index 00000000000..641ffaff385 --- /dev/null +++ b/src/lib/onboard/channel-state.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as onboardSession from "../state/onboard-session"; +import * as registry from "../state/registry"; + +type DisabledChannelsSession = Pick; + +export type DisabledChannelsDeps = { + loadSession: () => DisabledChannelsSession | null; + getRegistryDisabledChannels: (sandboxName: string) => string[]; +}; + +export function resolveDisabledChannels( + sandboxName: string, + deps?: DisabledChannelsDeps, +): string[] { + // `rebuild` destroys the registry entry before `onboard --resume` reaches + // createSandbox, so the session mirror is authoritative when present. + const sessionDisabledChannels = (deps?.loadSession ?? onboardSession.loadSession)() + ?.disabledChannels; + if (Array.isArray(sessionDisabledChannels)) { + return sessionDisabledChannels; + } + return (deps?.getRegistryDisabledChannels ?? registry.getDisabledChannels)(sandboxName); +} diff --git a/src/lib/state/onboard-session.test.ts b/src/lib/state/onboard-session.test.ts index de16e8be523..b2c925858fb 100644 --- a/src/lib/state/onboard-session.test.ts +++ b/src/lib/state/onboard-session.test.ts @@ -315,6 +315,48 @@ describe("onboard session", () => { expect(loaded.messagingChannels).toEqual(["telegram", "discord"]); }); + it("persists disabledChannels across save/load roundtrips", () => { + // Regression: `channels stop X` followed by rebuild must carry the paused + // set through the destroy/recreate window. The Session mirror is the only + // place this can survive, because rebuild destroys the registry entry + // before `onboard --resume` reads it back. + const created = session.createSession(); + created.disabledChannels = ["telegram"]; + session.saveSession(created); + + const loaded = requireLoadedSession(session.loadSession()); + expect(loaded.disabledChannels).toEqual(["telegram"]); + }); + + it("filters non-string entries out of persisted disabledChannels", () => { + const created = session.createSession(); + fs.mkdirSync(path.dirname(session.SESSION_FILE), { recursive: true }); + fs.writeFileSync( + session.SESSION_FILE, + JSON.stringify({ + ...created, + disabledChannels: ["telegram", 42, null, "discord"], + }), + ); + + const loaded = requireLoadedSession(session.loadSession()); + expect(loaded.disabledChannels).toEqual(["telegram", "discord"]); + }); + + it("defaults disabledChannels to null for fresh sessions", () => { + const fresh = session.createSession(); + expect(fresh.disabledChannels).toBeNull(); + }); + + it("filterSafeUpdates passes through disabledChannels and accepts explicit null clear", () => { + session.saveSession(session.createSession()); + session.markStepComplete("provider_selection", { disabledChannels: ["discord"] }); + expect(requireLoadedSession(session.loadSession()).disabledChannels).toEqual(["discord"]); + + session.markStepComplete("provider_selection", { disabledChannels: null }); + expect(requireLoadedSession(session.loadSession()).disabledChannels).toBeNull(); + }); + it("defaults messagingChannels to null for fresh sessions", () => { const fresh = session.createSession(); expect(fresh.messagingChannels).toBeNull(); diff --git a/src/lib/state/onboard-session.ts b/src/lib/state/onboard-session.ts index ac896220762..287eaa49c74 100644 --- a/src/lib/state/onboard-session.ts +++ b/src/lib/state/onboard-session.ts @@ -85,6 +85,14 @@ export interface Session { policyPresets: string[] | null; messagingChannels: string[] | null; messagingChannelConfig: MessagingChannelConfig | null; + // Channels the operator paused via `nemoclaw channels stop `. + // Mirrors `SandboxEntry.disabledChannels` so that `rebuild` — which + // destroys the registry entry before calling `onboard --resume` — + // can carry the paused set across the destroy/recreate window. + // Without this mirror, the disabledChannels filter inside createSandbox + // reads back `[]` from the freshly-empty registry and the channel + // comes back live after rebuild. See #(channels-stop-rebuild bug). + disabledChannels: string[] | null; // SHA-256 hex digest of every legacy credential value successfully // written to the OpenShell gateway during this onboard session, keyed by // env-name. Persisted across process restarts so a `--resume` run that @@ -153,6 +161,7 @@ export interface SessionUpdates { policyPresets?: string[]; messagingChannels?: string[]; messagingChannelConfig?: MessagingChannelConfig | null; + disabledChannels?: string[] | null; migratedLegacyValueHashes?: Record; gpuPassthrough?: boolean; telegramConfig?: TelegramConfig | null; @@ -355,6 +364,7 @@ export function createSession(overrides: Partial = {}): Session { policyPresets: readStringArray(overrides.policyPresets), messagingChannels: readStringArray(overrides.messagingChannels), messagingChannelConfig: sanitizeMessagingChannelConfig(overrides.messagingChannelConfig), + disabledChannels: readStringArray(overrides.disabledChannels), migratedLegacyValueHashes: overrides.migratedLegacyValueHashes ? readStringRecord(overrides.migratedLegacyValueHashes) : null, @@ -395,6 +405,7 @@ export function normalizeSession(data: Session | SessionJsonValue | undefined): policyPresets: readStringArray(data.policyPresets), messagingChannels: readStringArray(data.messagingChannels), messagingChannelConfig: sanitizeMessagingChannelConfig(data.messagingChannelConfig), + disabledChannels: readStringArray(data.disabledChannels), migratedLegacyValueHashes: readStringRecord(data.migratedLegacyValueHashes), gpuPassthrough: data.gpuPassthrough === true, telegramConfig: parseTelegramConfig(data.telegramConfig), @@ -816,6 +827,13 @@ export function filterSafeUpdates(updates: SessionUpdates): Partial { const messagingChannelConfig = sanitizeMessagingChannelConfig(updates.messagingChannelConfig); if (messagingChannelConfig) safe.messagingChannelConfig = messagingChannelConfig; } + if (updates.disabledChannels === null) { + safe.disabledChannels = null; + } else if (Array.isArray(updates.disabledChannels)) { + safe.disabledChannels = updates.disabledChannels.filter( + (value) => typeof value === "string", + ); + } if (isObject(updates.migratedLegacyValueHashes)) { const cleaned: Record = {}; for (const [k, v] of Object.entries(updates.migratedLegacyValueHashes)) { diff --git a/test/e2e/docs/parity-inventory.generated.json b/test/e2e/docs/parity-inventory.generated.json index 873e2d1f903..5b85bc02436 100644 --- a/test/e2e/docs/parity-inventory.generated.json +++ b/test/e2e/docs/parity-inventory.generated.json @@ -229,6 +229,379 @@ } ] }, + { + "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" + } + ] + }, { "script": "test/e2e/test-cloud-inference-e2e.sh", "assertions": [ @@ -15938,8 +16311,8 @@ } ], "totals": { - "scripts": 49, - "assertions": 1961, + "scripts": 50, + "assertions": 2007, "zero_assertion_scripts": 1 } } diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index 8f38500e210..2cb6aeb6017 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -385,6 +385,241 @@ scripts: reason: live legacy behavior requires non-deterministic infrastructure; retained for bucket parity tracking owner: e2e-maintainers secret_requirement: NVIDIA_API_KEY secret and network egress + test-channels-stop-start.sh: + scenario: ubuntu-repo-cloud-openclaw + status: not-started + 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 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 new file mode 100755 index 00000000000..33002284dab --- /dev/null +++ b/test/e2e/test-channels-stop-start.sh @@ -0,0 +1,421 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Channel stop/start 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. +# +# Telegram-only — Discord/Slack carry the same code path; this script covers +# the regression with the minimal channel surface. +# +# Prerequisites: +# - Docker running +# - NVIDIA_API_KEY set (real key or fake OpenAI endpoint) +# - NEMOCLAW_NON_INTERACTIVE=1, NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 +# +# Usage: +# NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ +# NVIDIA_API_KEY=nvapi-... bash test/e2e/test-channels-stop-start.sh + +set -uo pipefail + +export NEMOCLAW_E2E_DEFAULT_TIMEOUT=2400 +SCRIPT_DIR_TIMEOUT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +# shellcheck source=test/e2e/e2e-timeout.sh +source "${SCRIPT_DIR_TIMEOUT}/e2e-timeout.sh" + +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 + +pass() { + ((PASS++)) + ((TOTAL++)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail() { + ((FAIL++)) + ((TOTAL++)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} +skip() { + ((SKIP++)) + ((TOTAL++)) + printf '\033[33m SKIP: %s\033[0m\n' "$1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +print_summary() { + section "Summary" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + if [ "$FAIL" -gt 0 ]; then + echo "" + echo "FAILED" + exit 1 + fi + echo "" + if [ "$SKIP" -gt 0 ]; then + echo "PASSED (with $SKIP skipped)" + else + echo "ALL PASSED" + 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 + REPO="$(cd "$(dirname "$0")/../.." && pwd)" +else + echo "ERROR: Cannot find repo root." + exit 1 +fi + +SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-channels-stop-start}" +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}" + +# shellcheck source=test/e2e/lib/sandbox-teardown.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib/sandbox-teardown.sh" +register_sandbox_for_teardown "$SANDBOX_NAME" + +# ── 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 + + local result + result=$(timeout 60 ssh -F "$ssh_config" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=10 \ + -o LogLevel=ERROR \ + "openshell-${SANDBOX_NAME}" \ + "$cmd" \ + 2>&1) || true + + rm -f "$ssh_config" + 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 command -v jq >/dev/null 2>&1; then + jq -c --arg name "$SANDBOX_NAME" --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" + 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. + 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 + case "$out" in + *yes*) return 0 ;; + *no*) return 1 ;; + *) return 2 ;; + esac +} + +# ══════════════════════════════════════════════════════════════════ +# Phase 0: Prerequisites +# ══════════════════════════════════════════════════════════════════ +section "Phase 0: Prerequisites" + +if [ -z "${NVIDIA_API_KEY:-}" ]; then + fail "C0: NVIDIA_API_KEY is required" + print_summary +fi +pass "C0: NVIDIA_API_KEY is set" + +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" + +# ══════════════════════════════════════════════════════════════════ +# Phase 1: Install + onboard with Telegram enabled +# ══════════════════════════════════════════════════════════════════ +section "Phase 1: Install + onboard sandbox with Telegram" + +cd "$REPO" || exit 1 + +# 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 + +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 + +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 + +if ! openshell --version >/dev/null 2>&1; then + fail "C1c: openshell not on PATH after install" + print_summary +fi +pass "C1c: openshell installed" + +if ! command -v nemoclaw >/dev/null 2>&1; then + fail "C1d: nemoclaw not on PATH after install" + print_summary +fi +pass "C1d: nemoclaw installed" + +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 + +# ══════════════════════════════════════════════════════════════════ +# Phase 2: Verify baseline state (Telegram active) +# ══════════════════════════════════════════════════════════════════ +section "Phase 2: Verify baseline state (Telegram active)" + +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 + +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" + else + fail "C2b: openclaw.json missing 'telegram' channel before stop (precondition failed)" + 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 + +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 + +# ══════════════════════════════════════════════════════════════════ +# Phase 3: Stop telegram + rebuild +# ══════════════════════════════════════════════════════════════════ +section "Phase 3: channels stop telegram + rebuild" + +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 + +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 + +# ══════════════════════════════════════════════════════════════════ +# 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" + else + pass "C4a: openclaw.json excludes 'telegram' after stop+rebuild (#3453 fixed)" + 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 + +# 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 + +# 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})" +fi + +# ══════════════════════════════════════════════════════════════════ +# 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 +fi + +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 + print_summary +fi + +# ══════════════════════════════════════════════════════════════════ +# 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)" +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 +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 + +# 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 + +print_summary diff --git a/test/registry.test.ts b/test/registry.test.ts index 370d389a222..b2b8aee847b 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -227,6 +227,14 @@ describe("registry", () => { expect(persisted.sandboxes.s1.disabledChannels).toBeUndefined(); }); + it("updateSandbox clears disabledChannels when explicitly set to undefined", () => { + registry.registerSandbox({ name: "s1" }); + registry.setChannelDisabled("s1", "telegram", true); + expect(registry.updateSandbox("s1", { disabledChannels: undefined })).toBe(true); + const persisted = JSON.parse(fs.readFileSync(regFile, "utf-8")); + expect(persisted.sandboxes.s1.disabledChannels).toBeUndefined(); + }); + it("setChannelDisabled returns false when sandbox is missing", () => { expect(registry.setChannelDisabled("missing", "telegram", true)).toBe(false); });