Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/lib/messaging/plan-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,87 @@ describe("parseSandboxMessagingPlan", () => {
});
});

it("keeps compact persisted plans free of derived workflow sections", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required SPDX header to this file.

This changed .test.ts file is missing the repository-mandated SPDX copyright and license header.

Suggested header
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0

As per coding guidelines, **/*.{js,ts,tsx,sh} must include the SPDX copyright and Apache-2.0 license header.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("keeps compact persisted plans free of derived workflow sections", () => {
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/messaging/plan-validation.test.ts` at line 150, The new
plan-validation test file is missing the repository-required SPDX copyright and
Apache-2.0 license header. Add the standard SPDX header at the top of the file
before the first test in plan-validation.test.ts, following the same format used
in other JS/TS test files in the repository.

Source: Coding guidelines

const source = makePlan({
networkPolicy: {
presets: ["telegram"],
entries: [
{
channelId: "telegram",
presetName: "telegram",
policyKeys: ["telegram"],
source: "manifest",
},
],
},
agentRender: [
{
channelId: "telegram",
agent: "openclaw",
target: "openclaw.json",
kind: "json-fragment",
path: "channels.telegram",
value: { enabled: true },
templateRefs: [],
},
],
buildSteps: [
{
channelId: "telegram",
kind: "package-install",
outputId: "telegram-openclaw-plugin",
required: true,
value: "npm:@openclaw/telegram",
},
],
runtimeSetup: {
nodePreloads: [],
envAliases: [],
secretScans: [
{
channelId: "telegram",
path: "/sandbox/.openclaw/openclaw.json",
pattern: "TELEGRAM_BOT_TOKEN",
},
],
},
stateUpdates: [
{
channelId: "telegram",
kind: "persist-inputs",
stateKey: "allowedIds.telegram",
inputIds: ["allowedIds"],
},
],
healthChecks: [
{
channelId: "telegram",
phase: "health-check",
requiredBefore: "lifecycle-success",
hookIds: ["telegram-openclaw-bridge-health"],
},
],
});

const compact = compactSandboxMessagingPlanForPersistence(source);

expect(compact.networkPolicy).toEqual(source.networkPolicy);
expect(compact).not.toHaveProperty("agentRender");
expect(compact).not.toHaveProperty("buildSteps");
expect(compact).not.toHaveProperty("runtimeSetup");
expect(compact).not.toHaveProperty("stateUpdates");
expect(compact).not.toHaveProperty("healthChecks");
expect(compact.channels).toEqual([
{
channelId: "telegram",
active: true,
configured: true,
disabled: false,
inputs: [{ inputId: "allowedIds", value: "123" }],
},
]);
});

it("rejects mismatched selectors, duplicate channels, and unsupported channels", () => {
expect(parseSandboxMessagingPlan(makePlan(), { sandboxName: "other" })).toBeNull();
expect(parseSandboxMessagingPlan(makePlan(), { agent: "hermes" })).toBeNull();
Expand Down
17 changes: 8 additions & 9 deletions test/e2e-scenario/live/channels-add-remove.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,14 @@ function expectHostTelegramPlan(expected: "active" | "removed", context: string)
: {};
const networkEntries = planArray(networkPolicy, "entries");
const networkPresets = stringArray(networkPolicy.presets);
const agentRender = planArray(plan, "agentRender");

expect(Object.hasOwn(plan, "agentRender"), "messaging.plan.agentRender should not persist").toBe(
false,
);
expect(
channels.some((entry) => Object.hasOwn(entry, "hooks")),
"messaging.plan.channels hooks should not persist",
).toBe(false);

if (expected === "active") {
expect(
Expand All @@ -194,10 +201,6 @@ function expectHostTelegramPlan(expected: "active" | "removed", context: string)
),
`telegram TELEGRAM_BOT_TOKEN credential binding missing ${context}`,
).toBe(true);
expect(
agentRender.some((entry) => entry.channelId === "telegram" && entry.agent === "openclaw"),
`telegram openclaw agent render entry missing ${context}`,
).toBe(true);
expect(disabledChannels, `telegram unexpectedly disabled ${context}`).not.toContain("telegram");
return;
}
Expand All @@ -218,10 +221,6 @@ function expectHostTelegramPlan(expected: "active" | "removed", context: string)
credentialBindings.some((entry) => entry.channelId === "telegram"),
`telegram credential binding still present ${context}`,
).toBe(false);
expect(
agentRender.some((entry) => entry.channelId === "telegram"),
`telegram agent render entry still present ${context}`,
).toBe(false);
}

async function expectSandboxReady(
Expand Down
41 changes: 37 additions & 4 deletions test/e2e-scenario/live/messaging-providers-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ if env 2>/dev/null | grep -Fq "$token"; then echo FOUND; else echo ABSENT; fi`
? `token="$(printf '%s' ${shellQuote(tokenB64)} | base64 -d)"
if cat /proc/[0-9]*/cmdline 2>/dev/null | tr '\\0' '\\n' | grep -Fq "$token"; then echo FOUND; else echo ABSENT; fi`
: `token="$(printf '%s' ${shellQuote(tokenB64)} | base64 -d)"
if grep -rIlm1 -F "$token" /sandbox /home /etc /tmp /var 2>/dev/null | head -1; then true; else echo ABSENT; fi`;
match="$(grep -rIlm1 -F "$token" /sandbox /home /etc /tmp /var 2>/dev/null | head -1 || true)"
if [ -n "$match" ]; then printf '%s\n' "$match"; else echo ABSENT; fi`;
return sandboxOutput(sandbox, probe, artifactName, redactionValues);
}

Expand Down Expand Up @@ -797,24 +798,51 @@ function decodeFrame(buffer) {
if (buffer.length < 4) return null;
payloadLength = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLength === 127) {
if (buffer.length < 10) return null;
payloadLength = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
if (buffer.length < offset + payloadLength) return null;
return { opcode, payload: buffer.slice(offset, offset + payloadLength), totalLength: offset + payloadLength };
}

const socket = net.createConnection({ host, port });
function parseProxyTarget() {
const raw = process.env.HTTP_PROXY || process.env.http_proxy || "";
if (!raw) return null;
let parsed;
try {
parsed = new URL(raw);
} catch {
throw new Error("HTTP proxy for Discord Gateway proof is malformed");
}
if (parsed.protocol !== "http:") throw new Error("Discord Gateway proof only supports HTTP proxies");
const proxyPort = Number(parsed.port || "80");
if (!Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65535) throw new Error("HTTP proxy port for Discord Gateway proof is invalid");
if (parsed.hostname !== "10.200.0.1" || proxyPort !== 3128) throw new Error("unexpected HTTP proxy for Discord Gateway proof");
return { host: parsed.hostname, port: proxyPort };
}

const proxy = parseProxyTarget();
const socket = proxy
? net.createConnection({ host: proxy.host, port: proxy.port })
: net.createConnection({ host, port });
const timer = setTimeout(() => {
socket.destroy();
finish("TIMEOUT");
}, 20000);
let handshake = Buffer.alloc(0);
let framed = Buffer.alloc(0);
let upgraded = false;
let finished = false;

socket.on("connect", () => {
const key = crypto.randomBytes(16).toString("base64");
const requestTarget = proxy
? \`http://\${host}:\${port}/gateway?v=10&encoding=json\`
: "/gateway?v=10&encoding=json";
socket.write([
"GET /gateway?v=10&encoding=json HTTP/1.1",
\`GET \${requestTarget} HTTP/1.1\`,
\`Host: \${host}:\${port}\`,
"Upgrade: websocket",
"Connection: Upgrade",
Expand Down Expand Up @@ -864,14 +892,19 @@ socket.on("data", (chunk) => {
} else if (message.op === 11) {
results.push("HEARTBEAT_ACK");
clearTimeout(timer);
finished = true;
socket.end();
finish();
}
}
});
socket.on("error", (error) => {
clearTimeout(timer);
finish(\`ERROR \${error.message}\`);
if (!finished) finish(\`ERROR \${error.message}\`);
});
socket.on("close", () => {
clearTimeout(timer);
if (!finished) finish("CLOSED");
});
`,
{
Expand Down
29 changes: 27 additions & 2 deletions test/e2e-scenario/live/messaging-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,26 @@ process.exit(Array.isArray(channels) && channels.some((c) => c?.channelId === "w
);
expectExitZero(whatsappRebuild, "M-WA4: rebuild completed after WhatsApp channel add");

const whatsappPolicyPost = await runHost(
host,
"openshell",
["policy", "get", "--full", SANDBOX_NAME],
{
artifactName: "whatsapp-policy-post-rebuild-messaging-providers",
env: state.env,
redactionValues,
timeoutMs: 60_000,
},
);
const whatsappPolicyPostText = outputText(whatsappPolicyPost);
check(
policyTextHasHost(whatsappPolicyPostText, "web.whatsapp.com") &&
policyTextHasHost(whatsappPolicyPostText, "whatsapp.net") &&
policyTextHasHost(whatsappPolicyPostText, "raw.githubusercontent.com") &&
/\/usr\/local\/bin\/node|\/usr\/bin\/node/.test(whatsappPolicyPostText),
"M-WA5: WhatsApp policy preset survived rebuild with Node binary scope",
);

const providerList = await runHost(host, "openshell", ["provider", "list"], {
artifactName: "provider-list-messaging-providers",
env: state.env,
Expand Down Expand Up @@ -882,10 +902,15 @@ req.setTimeout(30000, () => { req.destroy(); console.log("TIMEOUT"); });
fakeGateway.captureFile,
(row) => row.event === "identify",
);
check(fs.existsSync(fakeGateway.captureFile), "M13f: fake Gateway capture file exists");
const gatewayCaptureText = fs.readFileSync(fakeGateway.captureFile, "utf8");
Comment on lines +905 to +906

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the file read with a hard precondition.

check() is a soft assertion here, so a missing capture file still falls through to readFileSync() and crashes with ENOENT. That turns the intended assertion into an opaque test abort and skips the rest of the diagnostics in this block.

Suggested fix
-    check(fs.existsSync(fakeGateway.captureFile), "M13f: fake Gateway capture file exists");
-    const gatewayCaptureText = fs.readFileSync(fakeGateway.captureFile, "utf8");
+    if (!fs.existsSync(fakeGateway.captureFile)) {
+      throw new Error("M13f: fake Gateway capture file exists");
+    }
+    const gatewayCaptureText = fs.readFileSync(fakeGateway.captureFile, "utf8");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
check(fs.existsSync(fakeGateway.captureFile), "M13f: fake Gateway capture file exists");
const gatewayCaptureText = fs.readFileSync(fakeGateway.captureFile, "utf8");
if (!fs.existsSync(fakeGateway.captureFile)) {
throw new Error("M13f: fake Gateway capture file exists");
}
const gatewayCaptureText = fs.readFileSync(fakeGateway.captureFile, "utf8");
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 905-905: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(fakeGateway.captureFile, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e-scenario/live/messaging-providers.test.ts` around lines 905 - 906,
The Gateway capture file existence check in messaging-providers.test.ts is
currently only a soft assertion, so execution can still reach the readFileSync
call and fail with ENOENT. Replace the check around fakeGateway.captureFile with
a hard precondition before reading, and keep the subsequent parsing/diagnostics
in the same gated block so the test fails at the intended assertion instead of
crashing later.

check(
gatewayIdentify?.tokenMatchesExpected === true &&
gatewayIdentify?.tokenLooksPlaceholder === false,
"M13f: fake Gateway received host-side Discord token after relay rewrite",
gatewayIdentify?.tokenLooksPlaceholder === false &&
!Object.prototype.hasOwnProperty.call(gatewayIdentify, "token") &&
!gatewayCaptureText.includes(state.tokens.discord) &&
!gatewayCaptureText.includes("openshell:resolve:env:"),
"M13f: fake Gateway proved placeholder-to-token rewrite without logging the raw token",
);

const gatewayPort = await sandboxOutput(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e-scenario/live/openclaw-discord-pairing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
expect(configSummary.token).toContain("openshell:resolve:env:");
expect(configSummary.token).toContain("DISCORD_BOT_TOKEN");
expect(configSummary.dmPolicy).not.toBe("allowlist");
expect(configSummary.accountProxy, "Discord account proxy").toMatch(/^http:\/\//);
expect(configSummary.accountProxy, "Discord account proxy").toBe("");
expect(configSummary.managedProxy, "OpenClaw managed proxy").toMatch(/^http:\/\//);

await assertOpenClawStateRoot(sandbox, SANDBOX_NAME, "discord", redactions);
Expand Down
Loading