diff --git a/scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs b/scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs index bc423ec65c8..92c8422398a 100644 --- a/scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs +++ b/scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs @@ -8,6 +8,46 @@ import { applyMockOpenAiModelConfig } from "../fixtures/mock-openai-config.mjs"; const command = process.argv[2]; const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); +const ansiEscapePattern = new RegExp(String.raw`\u001b\[[0-?]*[ -/]*[@-~]`, "g"); + +function stripAnsi(text) { + return text.replace(ansiEscapePattern, ""); +} + +const statusSectionTitles = new Set([ + "openclaw status", + "overview", + "plugin compatibility", + "model selection", + "security audit", + "channels", + "sessions", + "system events", + "health", + "usage", +]); + +function normalizedStatusHeading(line) { + return stripAnsi(line).trim().replace(/^#+\s*/, "").trim().toLowerCase(); +} + +function extractStatusSection(text, title) { + const target = title.toLowerCase(); + const lines = text.split(/\r?\n/); + const start = lines.findIndex((line) => normalizedStatusHeading(line) === target); + if (start === -1) { + return null; + } + const section = []; + for (const line of lines.slice(start + 1)) { + const normalized = normalizedStatusHeading(line); + if (normalized && statusSectionTitles.has(normalized)) { + break; + } + section.push(line); + } + return stripAnsi(section.join("\n")); +} function assertOnboardState() { const home = process.argv[3]; @@ -146,8 +186,14 @@ function assertStatusSurfaces() { if (!/channels/i.test(statusText)) { throw new Error(`plain status output did not render a Channels section. Output: ${statusText}`); } - if (!statusText.toLowerCase().includes(channel.toLowerCase())) { - throw new Error(`plain status output did not mention ${channel}. Output: ${statusText}`); + const channelsSection = extractStatusSection(statusText, "channels"); + if (!channelsSection) { + throw new Error(`plain status output did not render a Channels section. Output: ${statusText}`); + } + if (!channelsSection.toLowerCase().includes(channel.toLowerCase())) { + throw new Error( + `plain status output did not mention ${channel} in the Channels section. Output: ${statusText}`, + ); } } diff --git a/test/scripts/npm-onboard-channel-agent-assertions.test.ts b/test/scripts/npm-onboard-channel-agent-assertions.test.ts index e0426028e04..7b391cdfbdd 100644 --- a/test/scripts/npm-onboard-channel-agent-assertions.test.ts +++ b/test/scripts/npm-onboard-channel-agent-assertions.test.ts @@ -26,6 +26,25 @@ function runAssert(home: string, channel: string, ...tokens: string[]) { ); } +function runStatusAssert(channel: string, channelsStatus: unknown, statusText: string) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-status-assertions-")); + try { + const channelsStatusPath = path.join(tempDir, "channels-status.json"); + const statusTextPath = path.join(tempDir, "status.txt"); + fs.writeFileSync(channelsStatusPath, JSON.stringify(channelsStatus)); + fs.writeFileSync(statusTextPath, statusText); + return spawnSync( + process.execPath, + [assertionsPath, "assert-status-surfaces", channel, channelsStatusPath, statusTextPath], + { + encoding: "utf8", + }, + ); + } finally { + fs.rmSync(tempDir, { force: true, recursive: true }); + } +} + describe("npm onboard channel agent assertions", () => { it("validates channel tokens in their canonical config fields", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-assertions-")); @@ -59,4 +78,50 @@ describe("npm onboard channel agent assertions", () => { fs.rmSync(tempDir, { force: true, recursive: true }); } }); + + it("validates configured channels in the plain status Channels section", () => { + const result = runStatusAssert( + "telegram", + { configuredChannels: ["telegram"] }, + [ + "# OpenClaw status", + "", + "# Overview", + "OS macOS", + "", + "# Channels", + "Channel State Detail", + "telegram ok configured", + "", + "# Sessions", + "No sessions", + ].join("\n"), + ); + + expect(result.status).toBe(0); + }); + + it("rejects plain status output that mentions the channel outside the Channels section", () => { + const result = runStatusAssert( + "telegram", + { configuredChannels: ["telegram"] }, + [ + "# OpenClaw status", + "", + "# Overview", + "OS macOS", + "", + "# Channels", + "No channels configured", + "", + "# Sessions", + "telegram appeared in an unrelated session note", + ].join("\n"), + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "plain status output did not mention telegram in the Channels section", + ); + }); });