From 6fd8fb5aee552d16d0ebb76b86598538ef4a8d91 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 26 May 2026 07:37:12 +0800 Subject: [PATCH 01/10] fix(messaging): enable generated Slack channel config Signed-off-by: Chengjie Wang --- scripts/generate-openclaw-config.py | 5 ++++- test/generate-openclaw-config.test.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index 28dd98fd853..af0c2740f5b 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -577,7 +577,10 @@ def _placeholder(channel: str, env_key: str) -> str: "users": _allowed_ids[ch], } } - _ch_cfg[ch] = {"accounts": {"default": account}} + _ch_cfg[ch] = { + **({"enabled": True} if ch == "slack" else {}), + "accounts": {"default": account}, + } # WeChat (openclaw-weixin) is NOT added to channels.* here in build # contexts where the plugin has not been installed yet — writing it upfront diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index c0b33092ee3..892c54f1256 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -541,6 +541,7 @@ describe("generate-openclaw-config.py: config generation", () => { it("emits Bolt-shape placeholders for Slack so the SDK's prefix regex passes", () => { const channels = Buffer.from(JSON.stringify(["slack"])).toString("base64"); const config = runConfigScript({ NEMOCLAW_MESSAGING_CHANNELS_B64: channels }); + expect(config.channels.slack.enabled).toBe(true); const slack = config.channels.slack.accounts.default; // Bolt validates ^xoxb-[A-Za-z0-9_-]+$ / ^xapp-…$ at App construction. // OpenShell resolves these provider-shaped aliases at the egress boundary. From df339b8adda08c5b3ca56d3ae647e93aaef6e444 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 26 May 2026 07:37:12 +0800 Subject: [PATCH 02/10] fix(messaging): enable generated Slack channel config Signed-off-by: Chengjie Wang --- scripts/generate-openclaw-config.py | 5 ++++- test/e2e/test-messaging-providers.sh | 11 +++++++++++ test/generate-openclaw-config.test.ts | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index f3c8108ce66..950b14d2ae3 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -611,7 +611,10 @@ def _placeholder(channel: str, env_key: str) -> str: channel_id: dict(slack_channel_config) for channel_id in _slack_allowed_channels } - _ch_cfg[ch] = {"accounts": {"default": account}} + _ch_cfg[ch] = { + **({"enabled": True} if ch == "slack" else {}), + "accounts": {"default": account}, + } # WeChat (openclaw-weixin) is NOT added to channels.* here in build # contexts where the plugin has not been installed yet — writing it upfront diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 02094a65bfe..3927dd3bb64 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -1054,6 +1054,17 @@ print('yes' if 'slack' in d else 'no') if [ "$slack_configured" = "yes" ]; then pass "M11e: Slack channel configured with placeholder tokens (guard needed)" + sl_channel_enabled=$(echo "$channel_json" | python3 -c " +import json, sys +d = json.load(sys.stdin) +print('yes' if d.get('slack', {}).get('enabled') is True else 'no') +" 2>/dev/null || true) + if [ "$sl_channel_enabled" = "yes" ]; then + pass "M11e1: Slack channel is enabled at the top level for fresh startup discovery" + else + fail "M11e1: Slack channel missing top-level enabled=true; fresh startup may report disabled" + fi + # M11f/M11g/M11h: SLACK_ALLOWED_USERS should authorize both DMs and # channel @mentions from the same users. Config lives on the Slack account # because OpenClaw supports multi-account Slack channel policy. diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index 4264939f38d..805428d4400 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -541,6 +541,7 @@ describe("generate-openclaw-config.py: config generation", () => { it("emits Bolt-shape placeholders for Slack so the SDK's prefix regex passes", () => { const channels = Buffer.from(JSON.stringify(["slack"])).toString("base64"); const config = runConfigScript({ NEMOCLAW_MESSAGING_CHANNELS_B64: channels }); + expect(config.channels.slack.enabled).toBe(true); const slack = config.channels.slack.accounts.default; // Bolt validates ^xoxb-[A-Za-z0-9_-]+$ / ^xapp-…$ at App construction. // OpenShell resolves these provider-shaped aliases at the egress boundary. @@ -560,6 +561,7 @@ describe("generate-openclaw-config.py: config generation", () => { }); const slack = config.channels.slack.accounts.default; + expect(config.channels.slack.enabled).toBe(true); expect(slack.dmPolicy).toBe("allowlist"); expect(slack.allowFrom).toEqual(allowedUsers); expect(slack.groupPolicy).toBe("allowlist"); From bf5dd4695d6c5785146a1b1ae8ae5347d8aadf88 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 12:05:21 -0700 Subject: [PATCH 03/10] fix(messaging): activate Slack plugin config --- scripts/generate-openclaw-config.py | 2 ++ test/e2e/test-messaging-providers.sh | 20 ++++++++++++++++++++ test/generate-openclaw-config.test.ts | 2 ++ 3 files changed, 24 insertions(+) diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index 9f621af77fb..c49a4749630 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -720,6 +720,8 @@ def _placeholder(channel: str, env_key: str) -> str: # registered an accountId under channels.openclaw-weixin.accounts. "openclaw-weixin": {"enabled": True}, } + if "slack" in _ch_cfg: + plugin_entries["slack"] = {"enabled": True} _bundled_provider_plugins = { "amazon-bedrock": {"amazon-bedrock", "bedrock"}, "amazon-bedrock-mantle": {"amazon-bedrock-mantle"}, diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index a15880e5d4e..3492310fbef 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -861,6 +861,15 @@ try: except Exception as e: print(json.dumps({'error': str(e)})) \"" 2>/dev/null || true) +plugin_entries_json=$(sandbox_exec "python3 -c \" +import json, sys +try: + cfg = json.load(open('/sandbox/.openclaw/openclaw.json')) + entries = cfg.get('plugins', {}).get('entries', {}) + print(json.dumps(entries)) +except Exception as e: + print(json.dumps({'error': str(e)})) +\"" 2>/dev/null || true) if [ -z "$channel_json" ] || echo "$channel_json" | grep -q '"error"'; then fail "M6: Could not read openclaw.json channels (${channel_json:0:200})" @@ -1245,6 +1254,17 @@ print('yes' if d.get('slack', {}).get('enabled') is True else 'no') fail "M11e1: Slack channel missing top-level enabled=true; fresh startup may report disabled" fi + sl_plugin_enabled=$(echo "$plugin_entries_json" | python3 -c " +import json, sys +d = json.load(sys.stdin) +print('yes' if d.get('slack', {}).get('enabled') is True else 'no') +" 2>/dev/null || true) + if [ "$sl_plugin_enabled" = "yes" ]; then + pass "M11e2: Slack plugin entry is enabled so the external channel plugin activates" + else + fail "M11e2: Slack plugin entry missing plugins.entries.slack.enabled=true; OpenClaw may show Slack installed but disabled" + fi + # M11f/M11g/M11h: SLACK_ALLOWED_USERS should authorize both DMs and # channel @mentions from the same users. Config lives on the Slack account # because OpenClaw supports multi-account Slack channel policy. diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index f162b012dc6..b2a2b0cfab2 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -542,6 +542,7 @@ describe("generate-openclaw-config.py: config generation", () => { const channels = Buffer.from(JSON.stringify(["slack"])).toString("base64"); const config = runConfigScript({ NEMOCLAW_MESSAGING_CHANNELS_B64: channels }); expect(config.channels.slack.enabled).toBe(true); + expect(config.plugins.entries.slack).toEqual({ enabled: true }); const slack = config.channels.slack.accounts.default; // Bolt validates ^xoxb-[A-Za-z0-9_-]+$ / ^xapp-…$ at App construction. // OpenShell resolves these provider-shaped aliases at the egress boundary. @@ -562,6 +563,7 @@ describe("generate-openclaw-config.py: config generation", () => { const slack = config.channels.slack.accounts.default; expect(config.channels.slack.enabled).toBe(true); + expect(config.plugins.entries.slack).toEqual({ enabled: true }); expect(slack.dmPolicy).toBe("allowlist"); expect(slack.allowFrom).toEqual(allowedUsers); expect(slack.groupPolicy).toBe("allowlist"); From d6fbbb66c185c69e1714629d201cc538091d2f32 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 13:39:46 -0700 Subject: [PATCH 04/10] test(messaging): prove Slack runtime discovery --- test/e2e/test-messaging-providers.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 3492310fbef..77ef9a80e4e 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -1265,6 +1265,26 @@ print('yes' if d.get('slack', {}).get('enabled') is True else 'no') fail "M11e2: Slack plugin entry missing plugins.entries.slack.enabled=true; OpenClaw may show Slack installed but disabled" fi + sl_runtime_json=$(sandbox_exec "timeout 45 openclaw channels list --all --json --no-color 2>/dev/null" 2>/dev/null || true) + sl_runtime_state=$(echo "$sl_runtime_json" | python3 -c " +import json, sys +try: + d = json.load(sys.stdin) + slack = d.get('chat', {}).get('slack', {}) + accounts = slack.get('accounts', []) + if slack.get('installed') is True and slack.get('origin') == 'configured' and 'default' in accounts: + print('yes') + else: + print('no installed=%s origin=%s accounts=%s' % (slack.get('installed'), slack.get('origin'), accounts)) +except Exception as e: + print('error %s' % e) +" 2>/dev/null || true) + if [ "$sl_runtime_state" = "yes" ]; then + pass "M11e3: OpenClaw runtime discovery reports Slack installed and configured" + else + fail "M11e3: OpenClaw runtime discovery did not report Slack installed/configured (${sl_runtime_state}; output=${sl_runtime_json:0:300})" + fi + # M11f/M11g/M11h: SLACK_ALLOWED_USERS should authorize both DMs and # channel @mentions from the same users. Config lives on the Slack account # because OpenClaw supports multi-account Slack channel policy. From 299e33e8d4b4482ed7b5555773cec3047bf1087b Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 13:48:14 -0700 Subject: [PATCH 05/10] test(messaging): assert Slack status probe --- scripts/generate-openclaw-config.py | 3 ++ test/e2e/test-messaging-providers.sh | 50 +++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index c49a4749630..57ce5c9573e 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -721,6 +721,9 @@ def _placeholder(channel: str, env_key: str) -> str: "openclaw-weixin": {"enabled": True}, } if "slack" in _ch_cfg: + # OpenClaw treats external channel plugin activation as separate from + # channels.slack.enabled; keep this explicit until configured official + # channel plugins are auto-trusted by OpenClaw itself. plugin_entries["slack"] = {"enabled": True} _bundled_provider_plugins = { "amazon-bedrock": {"amazon-bedrock", "bedrock"}, diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 77ef9a80e4e..a4e611459e3 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -2259,7 +2259,47 @@ else fi fi -# S2: Dump gateway.log for diagnostics (must use openshell exec — SSH user +# S2: The running gateway must expose the Slack default account through +# OpenClaw's status path. With fake tokens, a Slack auth rejection is the +# expected startup outcome; the regression in #4189 was worse: Slack was absent +# from channel startup/status entirely. +sl_status_json=$(sandbox_exec "timeout 45 openclaw channels status --probe --channel slack --json --no-color --timeout 15000 2>/dev/null" 2>/dev/null || true) +sl_status_state=$(echo "$sl_status_json" | python3 -c " +import json, re, sys +try: + d = json.load(sys.stdin) + accounts = d.get('channelAccounts', {}).get('slack', []) + account = next((a for a in accounts if a.get('accountId') == 'default'), accounts[0] if accounts else {}) + configured = account.get('configured') is True + enabled = account.get('enabled') is True + running = account.get('running') is True or account.get('connected') is True + probe = account.get('probe') if isinstance(account.get('probe'), dict) else {} + text = ' '.join( + str(v or '') + for v in ( + account.get('lastError'), + probe.get('error'), + probe.get('status'), + ) + ) + expected_auth_failure = bool(re.search(r'invalid_auth|not_authed|not_allowed_token_type|auth', text, re.I)) + if configured and enabled and (running or expected_auth_failure): + print('yes running=%s expected_auth_failure=%s' % (running, expected_auth_failure)) + else: + print( + 'no configured=%s enabled=%s running=%s expected_auth_failure=%s accounts=%s' + % (configured, enabled, running, expected_auth_failure, accounts) + ) +except Exception as e: + print('error %s' % e) +" 2>/dev/null || true) +if echo "$sl_status_state" | grep -q "^yes "; then + pass "S2: OpenClaw status probe reports Slack default configured/enabled on the running gateway (${sl_status_state})" +else + fail "S2: OpenClaw status probe did not report Slack default on the running gateway (${sl_status_state}; output=${sl_status_json:0:500})" +fi + +# S3: Dump gateway.log for diagnostics (must use openshell exec — SSH user # cannot read the file because it's 600 gateway:gateway). gw_log=$(openshell sandbox exec --name "$SANDBOX_NAME" -- cat /tmp/gateway.log 2>/dev/null || true) if [ -z "$gw_log" ]; then @@ -2273,14 +2313,14 @@ echo "$gw_log" | tail -30 | while IFS= read -r line; do done if echo "$gw_log" | grep -q "provider failed to start:.*gateway continues"; then - pass "S2: Gateway log shows Slack rejection was caught by channel guard" + pass "S3: Gateway log shows Slack rejection was caught by channel guard" elif echo "$gw_log" | grep -qi "slack"; then info "Slack-related lines: $(echo "$gw_log" | grep -i slack | head -5)" - skip "S2: Gateway log has Slack output but not the guard catch message" + skip "S3: Gateway log has Slack output but not the guard catch message" elif [ -z "$gw_log" ]; then - skip "S2: Could not read gateway log (container may have exited)" + skip "S3: Could not read gateway log (container may have exited)" else - skip "S2: No Slack-related output in gateway log" + skip "S3: No Slack-related output in gateway log" fi # ══════════════════════════════════════════════════════════════════ From 63bbc5cc0568a8da53302c362ab478b03fd14376 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 14:58:27 -0700 Subject: [PATCH 06/10] test(messaging): accept Slack startup log proof --- test/e2e/test-messaging-providers.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index a4e611459e3..5dca617158a 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -2267,7 +2267,18 @@ sl_status_json=$(sandbox_exec "timeout 45 openclaw channels status --probe --cha sl_status_state=$(echo "$sl_status_json" | python3 -c " import json, re, sys try: - d = json.load(sys.stdin) + raw = sys.stdin.read() + start = raw.find('{') + if start < 0: + raise ValueError('no JSON object in status output') + d = json.JSONDecoder().raw_decode(raw[start:])[0] + if d.get('gatewayReachable') is False and d.get('configOnly') is True: + configured = d.get('configuredChannels', []) + if 'slack' in configured: + print('config-only slack-configured') + else: + print('config-only slack-missing configuredChannels=%s' % configured) + sys.exit(0) accounts = d.get('channelAccounts', {}).get('slack', []) account = next((a for a in accounts if a.get('accountId') == 'default'), accounts[0] if accounts else {}) configured = account.get('configured') is True @@ -2295,6 +2306,8 @@ except Exception as e: " 2>/dev/null || true) if echo "$sl_status_state" | grep -q "^yes "; then pass "S2: OpenClaw status probe reports Slack default configured/enabled on the running gateway (${sl_status_state})" +elif [ "$sl_status_state" = "config-only slack-configured" ]; then + skip "S2: OpenClaw status probe fell back to config-only status but still lists Slack as configured" else fail "S2: OpenClaw status probe did not report Slack default on the running gateway (${sl_status_state}; output=${sl_status_json:0:500})" fi @@ -2312,7 +2325,10 @@ echo "$gw_log" | tail -30 | while IFS= read -r line; do info " $line" done -if echo "$gw_log" | grep -q "provider failed to start:.*gateway continues"; then +if echo "$gw_log" | grep -q "\\[slack\\] \\[default\\] starting provider" \ + && echo "$gw_log" | grep -qi "invalid_auth"; then + pass "S3: Gateway log shows Slack plugin started and reached the expected fake-token auth path" +elif echo "$gw_log" | grep -q "provider failed to start:.*gateway continues"; then pass "S3: Gateway log shows Slack rejection was caught by channel guard" elif echo "$gw_log" | grep -qi "slack"; then info "Slack-related lines: $(echo "$gw_log" | grep -i slack | head -5)" From 98d4dc1a2e68fca6a02bd9c6cf8b450f144c53ac Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 16:18:12 -0700 Subject: [PATCH 07/10] fix(messaging): keep Slack config guardrail neutral --- scripts/generate-openclaw-config.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index a2873bc6d51..fdef5e53d8b 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -611,10 +611,7 @@ def _placeholder(channel: str, env_key: str) -> str: channel_id: dict(slack_channel_config) for channel_id in _slack_allowed_channels } - _ch_cfg[ch] = { - **({"enabled": True} if ch == "slack" else {}), - "accounts": {"default": account}, - } + _ch_cfg[ch] = {**({"enabled": True} if ch == "slack" else {}), "accounts": {"default": account}} # WeChat (openclaw-weixin) is NOT added to channels.* here in build # contexts where the plugin has not been installed yet — writing it upfront @@ -633,7 +630,6 @@ def _placeholder(channel: str, env_key: str) -> str: # framework allowFrom file at credentials/openclaw-weixin-{accountId}- # allowFrom.json — not the openclaw.json accounts..allowFrom mechanism # that telegram/discord/slack use. - if "discord" in _ch_cfg and _discord_guilds: _ch_cfg["discord"].update( {"groupPolicy": "allowlist", "guilds": _discord_guilds} @@ -720,11 +716,7 @@ def _placeholder(channel: str, env_key: str) -> str: # registered an accountId under channels.openclaw-weixin.accounts. "openclaw-weixin": {"enabled": True}, } - if "slack" in _ch_cfg: - # OpenClaw treats external channel plugin activation as separate from - # channels.slack.enabled; keep this explicit until configured official - # channel plugins are auto-trusted by OpenClaw itself. - plugin_entries["slack"] = {"enabled": True} + plugin_entries.update({"slack": {"enabled": True}} if "slack" in _ch_cfg else {}) _bundled_provider_plugins = { "amazon-bedrock": {"amazon-bedrock", "bedrock"}, "amazon-bedrock-mantle": {"amazon-bedrock-mantle"}, From 977676b2cf6b6d8be105cfd944e4c97e1c3ccdde Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 16:21:34 -0700 Subject: [PATCH 08/10] test(messaging): move Slack proof to scenario e2e --- .../slack/00-slack-provider-state.sh | 11 ++ test/e2e/test-messaging-providers.sh | 119 +----------------- 2 files changed, 17 insertions(+), 113 deletions(-) diff --git a/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh b/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh index baa1e57f323..a7bab62a753 100755 --- a/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh +++ b/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh @@ -11,4 +11,15 @@ case "${provider}" in *) e2e_fail "expected-state.messaging.slack.provider-state expected slack provider, got ${provider}" ;; esac e2e_messaging_assert_provider_attached +if [[ "$(e2e_context_get E2E_AGENT)" == "openclaw" ]]; then + if [[ -n "${E2E_DRY_RUN:-}" ]]; then + e2e_pass "expected-state.messaging.slack.openclaw-enabled dry-run" + else + content="$(e2e_messaging_read_config_surface)" + if ! printf '%s\n' "${content}" | python3 -c 'import json, sys; cfg=json.load(sys.stdin); assert cfg["channels"]["slack"]["enabled"] is True; assert cfg["plugins"]["entries"]["slack"]["enabled"] is True'; then + e2e_fail "expected-state.messaging.slack.openclaw-enabled missing channels.slack.enabled or plugins.entries.slack.enabled" + fi + e2e_pass "expected-state.messaging.slack.openclaw-enabled channel and plugin enabled" + fi +fi e2e_pass "expected-state.messaging.slack.provider-state ${provider} provider state configured" diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 5dca617158a..ad0899ef159 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -861,15 +861,6 @@ try: except Exception as e: print(json.dumps({'error': str(e)})) \"" 2>/dev/null || true) -plugin_entries_json=$(sandbox_exec "python3 -c \" -import json, sys -try: - cfg = json.load(open('/sandbox/.openclaw/openclaw.json')) - entries = cfg.get('plugins', {}).get('entries', {}) - print(json.dumps(entries)) -except Exception as e: - print(json.dumps({'error': str(e)})) -\"" 2>/dev/null || true) if [ -z "$channel_json" ] || echo "$channel_json" | grep -q '"error"'; then fail "M6: Could not read openclaw.json channels (${channel_json:0:200})" @@ -1243,48 +1234,6 @@ print('yes' if 'slack' in d else 'no') if [ "$slack_configured" = "yes" ]; then pass "M11e: Slack channel configured with placeholder tokens (guard needed)" - sl_channel_enabled=$(echo "$channel_json" | python3 -c " -import json, sys -d = json.load(sys.stdin) -print('yes' if d.get('slack', {}).get('enabled') is True else 'no') -" 2>/dev/null || true) - if [ "$sl_channel_enabled" = "yes" ]; then - pass "M11e1: Slack channel is enabled at the top level for fresh startup discovery" - else - fail "M11e1: Slack channel missing top-level enabled=true; fresh startup may report disabled" - fi - - sl_plugin_enabled=$(echo "$plugin_entries_json" | python3 -c " -import json, sys -d = json.load(sys.stdin) -print('yes' if d.get('slack', {}).get('enabled') is True else 'no') -" 2>/dev/null || true) - if [ "$sl_plugin_enabled" = "yes" ]; then - pass "M11e2: Slack plugin entry is enabled so the external channel plugin activates" - else - fail "M11e2: Slack plugin entry missing plugins.entries.slack.enabled=true; OpenClaw may show Slack installed but disabled" - fi - - sl_runtime_json=$(sandbox_exec "timeout 45 openclaw channels list --all --json --no-color 2>/dev/null" 2>/dev/null || true) - sl_runtime_state=$(echo "$sl_runtime_json" | python3 -c " -import json, sys -try: - d = json.load(sys.stdin) - slack = d.get('chat', {}).get('slack', {}) - accounts = slack.get('accounts', []) - if slack.get('installed') is True and slack.get('origin') == 'configured' and 'default' in accounts: - print('yes') - else: - print('no installed=%s origin=%s accounts=%s' % (slack.get('installed'), slack.get('origin'), accounts)) -except Exception as e: - print('error %s' % e) -" 2>/dev/null || true) - if [ "$sl_runtime_state" = "yes" ]; then - pass "M11e3: OpenClaw runtime discovery reports Slack installed and configured" - else - fail "M11e3: OpenClaw runtime discovery did not report Slack installed/configured (${sl_runtime_state}; output=${sl_runtime_json:0:300})" - fi - # M11f/M11g/M11h: SLACK_ALLOWED_USERS should authorize both DMs and # channel @mentions from the same users. Config lives on the Slack account # because OpenClaw supports multi-account Slack channel policy. @@ -2259,60 +2208,7 @@ else fi fi -# S2: The running gateway must expose the Slack default account through -# OpenClaw's status path. With fake tokens, a Slack auth rejection is the -# expected startup outcome; the regression in #4189 was worse: Slack was absent -# from channel startup/status entirely. -sl_status_json=$(sandbox_exec "timeout 45 openclaw channels status --probe --channel slack --json --no-color --timeout 15000 2>/dev/null" 2>/dev/null || true) -sl_status_state=$(echo "$sl_status_json" | python3 -c " -import json, re, sys -try: - raw = sys.stdin.read() - start = raw.find('{') - if start < 0: - raise ValueError('no JSON object in status output') - d = json.JSONDecoder().raw_decode(raw[start:])[0] - if d.get('gatewayReachable') is False and d.get('configOnly') is True: - configured = d.get('configuredChannels', []) - if 'slack' in configured: - print('config-only slack-configured') - else: - print('config-only slack-missing configuredChannels=%s' % configured) - sys.exit(0) - accounts = d.get('channelAccounts', {}).get('slack', []) - account = next((a for a in accounts if a.get('accountId') == 'default'), accounts[0] if accounts else {}) - configured = account.get('configured') is True - enabled = account.get('enabled') is True - running = account.get('running') is True or account.get('connected') is True - probe = account.get('probe') if isinstance(account.get('probe'), dict) else {} - text = ' '.join( - str(v or '') - for v in ( - account.get('lastError'), - probe.get('error'), - probe.get('status'), - ) - ) - expected_auth_failure = bool(re.search(r'invalid_auth|not_authed|not_allowed_token_type|auth', text, re.I)) - if configured and enabled and (running or expected_auth_failure): - print('yes running=%s expected_auth_failure=%s' % (running, expected_auth_failure)) - else: - print( - 'no configured=%s enabled=%s running=%s expected_auth_failure=%s accounts=%s' - % (configured, enabled, running, expected_auth_failure, accounts) - ) -except Exception as e: - print('error %s' % e) -" 2>/dev/null || true) -if echo "$sl_status_state" | grep -q "^yes "; then - pass "S2: OpenClaw status probe reports Slack default configured/enabled on the running gateway (${sl_status_state})" -elif [ "$sl_status_state" = "config-only slack-configured" ]; then - skip "S2: OpenClaw status probe fell back to config-only status but still lists Slack as configured" -else - fail "S2: OpenClaw status probe did not report Slack default on the running gateway (${sl_status_state}; output=${sl_status_json:0:500})" -fi - -# S3: Dump gateway.log for diagnostics (must use openshell exec — SSH user +# S2: Dump gateway.log for diagnostics (must use openshell exec — SSH user # cannot read the file because it's 600 gateway:gateway). gw_log=$(openshell sandbox exec --name "$SANDBOX_NAME" -- cat /tmp/gateway.log 2>/dev/null || true) if [ -z "$gw_log" ]; then @@ -2325,18 +2221,15 @@ echo "$gw_log" | tail -30 | while IFS= read -r line; do info " $line" done -if echo "$gw_log" | grep -q "\\[slack\\] \\[default\\] starting provider" \ - && echo "$gw_log" | grep -qi "invalid_auth"; then - pass "S3: Gateway log shows Slack plugin started and reached the expected fake-token auth path" -elif echo "$gw_log" | grep -q "provider failed to start:.*gateway continues"; then - pass "S3: Gateway log shows Slack rejection was caught by channel guard" +if echo "$gw_log" | grep -q "provider failed to start:.*gateway continues"; then + pass "S2: Gateway log shows Slack rejection was caught by channel guard" elif echo "$gw_log" | grep -qi "slack"; then info "Slack-related lines: $(echo "$gw_log" | grep -i slack | head -5)" - skip "S3: Gateway log has Slack output but not the guard catch message" + skip "S2: Gateway log has Slack output but not the guard catch message" elif [ -z "$gw_log" ]; then - skip "S3: Could not read gateway log (container may have exited)" + skip "S2: Could not read gateway log (container may have exited)" else - skip "S3: No Slack-related output in gateway log" + skip "S2: No Slack-related output in gateway log" fi # ══════════════════════════════════════════════════════════════════ From 4be23c067ae3fa32cdcb6604bd51a9b6e79523a8 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 16:24:24 -0700 Subject: [PATCH 09/10] test(e2e): move Slack runtime proof to scenarios --- .../framework-tests/e2e-suite-runner.test.ts | 1 + .../helpers/emit-context-from-plan.sh | 4 +++ .../lib/messaging_providers.sh | 10 +++++++ .../slack/00-slack-provider-state.sh | 30 ++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/test/e2e-scenario/framework-tests/e2e-suite-runner.test.ts b/test/e2e-scenario/framework-tests/e2e-suite-runner.test.ts index 5a917853f83..ded16c19179 100644 --- a/test/e2e-scenario/framework-tests/e2e-suite-runner.test.ts +++ b/test/e2e-scenario/framework-tests/e2e-suite-runner.test.ts @@ -90,6 +90,7 @@ describe("Issue #3810 messaging suite wiring", () => { "telegram-injection-safety", "discord-gateway-path", "slack-provider-state", + "slack.runtime-discovery", ]) { expect(output).toContain(id); } diff --git a/test/e2e-scenario/nemoclaw_scenarios/helpers/emit-context-from-plan.sh b/test/e2e-scenario/nemoclaw_scenarios/helpers/emit-context-from-plan.sh index 7fa95e11b60..888979e4dc4 100755 --- a/test/e2e-scenario/nemoclaw_scenarios/helpers/emit-context-from-plan.sh +++ b/test/e2e-scenario/nemoclaw_scenarios/helpers/emit-context-from-plan.sh @@ -56,6 +56,7 @@ ONBOARDING_PATH="$(read_plan_value dimensions.onboarding.profile.path)" AGENT="$(read_plan_value dimensions.onboarding.profile.agent)" PROVIDER="$(read_plan_value dimensions.onboarding.profile.provider)" INFERENCE_ROUTE="$(read_plan_value dimensions.onboarding.profile.inference_route)" +MESSAGING_PROVIDER="$(read_plan_value dimensions.onboarding.profile.messaging)" : "${PLATFORM_OS:=unknown}" : "${EXECUTION_TARGET:=local}" @@ -77,6 +78,9 @@ e2e_context_set E2E_ONBOARDING_PATH "${ONBOARDING_PATH}" e2e_context_set E2E_AGENT "${AGENT}" e2e_context_set E2E_PROVIDER "${PROVIDER}" e2e_context_set E2E_INFERENCE_ROUTE "${INFERENCE_ROUTE}" +if [[ -n "${MESSAGING_PROVIDER}" ]]; then + e2e_context_set E2E_MESSAGING_PROVIDER "${MESSAGING_PROVIDER}" +fi # Sandbox name and gateway URL are normally discovered/assigned by # onboarding. Seed them here so dry-run consumers can exercise the suite diff --git a/test/e2e-scenario/validation_suites/lib/messaging_providers.sh b/test/e2e-scenario/validation_suites/lib/messaging_providers.sh index 03c85ae6c2b..3f61aea2daa 100755 --- a/test/e2e-scenario/validation_suites/lib/messaging_providers.sh +++ b/test/e2e-scenario/validation_suites/lib/messaging_providers.sh @@ -112,6 +112,16 @@ e2e_messaging_read_config_surface() { cat "${path}" return 0 fi + local sandbox_name + sandbox_name="$(e2e_context_get E2E_SANDBOX_NAME)" + if [[ -n "${sandbox_name}" && "${path}" == /sandbox/* ]]; then + local remote + remote="$(openshell sandbox exec --name "${sandbox_name}" -- cat "${path}" 2>/dev/null || true)" + if [[ -n "${remote}" ]]; then + printf '%s\n' "${remote}" + return 0 + fi + fi e2e_fail "expected-state.messaging.config-surface missing config content/path for ${path}" } diff --git a/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh b/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh index a7bab62a753..0f1afa2e144 100755 --- a/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh +++ b/test/e2e-scenario/validation_suites/messaging/slack/00-slack-provider-state.sh @@ -14,12 +14,40 @@ e2e_messaging_assert_provider_attached if [[ "$(e2e_context_get E2E_AGENT)" == "openclaw" ]]; then if [[ -n "${E2E_DRY_RUN:-}" ]]; then e2e_pass "expected-state.messaging.slack.openclaw-enabled dry-run" + e2e_pass "expected-state.messaging.slack.runtime-discovery dry-run" else content="$(e2e_messaging_read_config_surface)" - if ! printf '%s\n' "${content}" | python3 -c 'import json, sys; cfg=json.load(sys.stdin); assert cfg["channels"]["slack"]["enabled"] is True; assert cfg["plugins"]["entries"]["slack"]["enabled"] is True'; then + if ! printf '%s\n' "${content}" | python3 -c ' +import json +import sys +cfg = json.load(sys.stdin) +assert cfg["channels"]["slack"]["enabled"] is True +assert cfg["plugins"]["entries"]["slack"]["enabled"] is True +'; then e2e_fail "expected-state.messaging.slack.openclaw-enabled missing channels.slack.enabled or plugins.entries.slack.enabled" fi e2e_pass "expected-state.messaging.slack.openclaw-enabled channel and plugin enabled" + + sandbox_name="$(e2e_context_get E2E_SANDBOX_NAME)" + runtime_json="$(openshell sandbox exec --name "${sandbox_name}" -- timeout 45 openclaw channels list --all --json --no-color 2>/dev/null || true)" + runtime_state="$(printf '%s\n' "${runtime_json}" | python3 -c ' +import json +import sys +try: + data = json.load(sys.stdin) + slack = data.get("chat", {}).get("slack", {}) + accounts = slack.get("accounts", []) + if slack.get("installed") is True and slack.get("origin") == "configured" and "default" in accounts: + print("yes") + else: + print("no installed=%s origin=%s accounts=%s" % (slack.get("installed"), slack.get("origin"), accounts)) +except Exception as exc: + print("error %s" % exc) +' 2>/dev/null || true)" + if [[ "${runtime_state}" != "yes" ]]; then + e2e_fail "expected-state.messaging.slack.runtime-discovery OpenClaw did not report Slack installed/configured (${runtime_state}; output=${runtime_json:0:300})" + fi + e2e_pass "expected-state.messaging.slack.runtime-discovery OpenClaw reports Slack installed and configured" fi fi e2e_pass "expected-state.messaging.slack.provider-state ${provider} provider state configured" From ffd2c0da93201d3dcb176b6402b178f39d467856 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Wed, 27 May 2026 16:36:45 -0700 Subject: [PATCH 10/10] test(e2e): bound remote messaging config read --- test/e2e-scenario/validation_suites/lib/messaging_providers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-scenario/validation_suites/lib/messaging_providers.sh b/test/e2e-scenario/validation_suites/lib/messaging_providers.sh index 3f61aea2daa..77eb1f1176d 100755 --- a/test/e2e-scenario/validation_suites/lib/messaging_providers.sh +++ b/test/e2e-scenario/validation_suites/lib/messaging_providers.sh @@ -116,7 +116,7 @@ e2e_messaging_read_config_surface() { sandbox_name="$(e2e_context_get E2E_SANDBOX_NAME)" if [[ -n "${sandbox_name}" && "${path}" == /sandbox/* ]]; then local remote - remote="$(openshell sandbox exec --name "${sandbox_name}" -- cat "${path}" 2>/dev/null || true)" + remote="$(timeout 30 openshell sandbox exec --name "${sandbox_name}" -- cat "${path}" 2>/dev/null || true)" if [[ -n "${remote}" ]]; then printf '%s\n' "${remote}" return 0