Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
b09dedd
fix(e2e): restore fake API port publishing
jyaunches Aug 31, 2026
20fd192
fix(e2e): isolate fake API port proxy
jyaunches Aug 31, 2026
fc8bc2b
fix(e2e): verify fake API proxy readiness
jyaunches Aug 31, 2026
bb2f0b9
fix(e2e): use native proxy traffic probes
jyaunches Aug 31, 2026
34fb3a8
fix(e2e): complete fake Slack ownership
jyaunches Aug 31, 2026
e1a64ca
test(e2e): bound fake Slack runtime proof
jyaunches Aug 31, 2026
2bd195b
test(e2e): share Slack pairing fixture
jyaunches Aug 31, 2026
6bf4881
test(e2e): map Slack fixture proof
jyaunches Aug 31, 2026
fa028aa
test(e2e): assert distinct Slack ports
jyaunches Aug 31, 2026
7a7cad0
merge: resolve conflicts with main
github-actions[bot] Sep 1, 2026
bfd48e4
fix(e2e): clean up fake API proxy merge
jyaunches Sep 1, 2026
b1fe44e
merge: refresh with main
jyaunches Sep 1, 2026
da3e670
fix(e2e): diagnose fake API startup failures
jyaunches Sep 1, 2026
093dbe3
test(e2e): assert structured Slack traffic captures
jyaunches Sep 1, 2026
7ef3742
fix(e2e): require fake API REST readiness reply
jyaunches Sep 1, 2026
23325c3
Merge branch 'main' into codex/fix-e2e-internal-fake-api-ports
prekshivyas Sep 1, 2026
90bd8da
test(e2e): align fake API runtime proof ownership
prekshivyas Sep 1, 2026
58fb3f9
merge: resolve conflicts with main
github-actions[bot] Sep 1, 2026
7d544dd
test(e2e): preserve runtime proof behavior boundaries
prekshivyas Sep 1, 2026
8454937
merge: update main
prekshivyas Sep 1, 2026
e735e7c
test(e2e): bound process-token runtime proof
prekshivyas Sep 1, 2026
0e94f81
merge: update main
prekshivyas Sep 1, 2026
dbd011c
test(e2e): cover fake API readiness diagnostics
prekshivyas Sep 1, 2026
ed71acf
test(e2e): preserve Slack rebuild policy bindings
prekshivyas Sep 1, 2026
5f2f3ce
merge: integrate upstream main
prekshivyas Sep 1, 2026
98ffcd0
test(e2e): isolate fake API proxy network
prekshivyas Sep 1, 2026
c984600
test(e2e): verify fake API trust boundaries
prekshivyas Sep 1, 2026
2757424
test(e2e): use owned Slack policy on rebuild
prekshivyas Sep 1, 2026
121c428
fix(e2e): publish proxy through bridge network
prekshivyas Sep 1, 2026
603c6ee
test(e2e): decouple diagnostic call order
prekshivyas Sep 1, 2026
141aaa9
fix(e2e): bind proxy to OpenShell bridge
prekshivyas Sep 1, 2026
d1fbe5b
test(e2e): own proxy traffic proof at runtime
prekshivyas Sep 1, 2026
b4b19c0
test(e2e): resolve WeChat runtime inventory
prekshivyas Sep 1, 2026
db5a9c6
refactor(e2e): neutralize policy binding owner
prekshivyas Sep 1, 2026
a486db6
test(e2e): declare pairing helper owners
prekshivyas Sep 1, 2026
ed4a691
fix(e2e): isolate credential forwarding proxies
prekshivyas Sep 1, 2026
5b30a53
test(e2e): clarify proxy publication contract
prekshivyas Sep 1, 2026
6cbbd33
refactor(e2e): share raw token surface probes
prekshivyas Sep 1, 2026
68bf223
fix(e2e): migrate Hermes fake gateway host
prekshivyas Sep 1, 2026
72457a2
refactor(e2e): share secondary cleanup owner
prekshivyas Sep 1, 2026
4e9b1ba
docs(e2e): clarify messaging token precedence
prekshivyas Sep 1, 2026
492606a
test(e2e): assert fake API policy behavior
prekshivyas Sep 1, 2026
3903ba4
test(e2e): map shared messaging cleanup owner
prekshivyas Sep 1, 2026
15ad4d3
test(e2e): reduce policy proof scaffolding
prekshivyas Sep 1, 2026
0031d8e
test(e2e): clarify cleanup failure contract
prekshivyas Sep 1, 2026
150f7d8
fix(e2e): keep fake API credentials out of argv
prekshivyas Sep 1, 2026
1fc3899
fix(e2e): publish fake API proxy from bridge
prekshivyas Sep 1, 2026
4a59cc1
merge: integrate upstream main
prekshivyas Sep 1, 2026
c5bee38
fix(e2e): avoid fake API gateway hairpin
prekshivyas Sep 1, 2026
a0df2ef
fix(e2e): isolate fake API credentials
prekshivyas Sep 1, 2026
b0e17df
fix(e2e): fail closed on fixture checks
prekshivyas Sep 1, 2026
fb01ee9
refactor(e2e): inline policy build diagnostics
prekshivyas Sep 1, 2026
956387f
test(e2e): clarify fake API test ownership
prekshivyas Sep 2, 2026
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
25 changes: 25 additions & 0 deletions test/automation/pull-requests/pr-risk-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,31 @@ describe("deterministic PR risk plan", () => {
expect(result.requiredJobs).toEqual([]);
});

it.each([
"test/e2e/lib/fake-api-port-proxy.mts",
"test/e2e/lib/fake-api-port-readiness.mts",
"test/e2e/live/messaging-providers-helpers.ts",
])("selects every fake Docker API consumer for %s", (changedFile) => {
expect(focusedE2eJobsForChangedFiles([changedFile])).toEqual(
[
"hermes-discord",
"messaging-providers",
"openclaw-discord-pairing",
"openclaw-slack-pairing",
].map((id) => ({ id, matchedFiles: [changedFile] })),
);
});

it("selects both fake Slack API consumers", () => {
const changedFile = "test/e2e/lib/fake-slack-api.cjs";
expect(focusedE2eJobsForChangedFiles([changedFile])).toEqual(
["messaging-providers", "openclaw-slack-pairing"].map((id) => ({
id,
matchedFiles: [changedFile],
})),
);
});

it("maps a shared gateway live test to every catalogue fixture (#7921)", () => {
const changedFiles = ["test/e2e/live/openshell-gateway-upgrade.test.ts"];
const focusedE2eJobs = catalogueTargetsForChangedFiles(changedFiles).map((target) => ({
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/fixtures/host-mock-firewall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface DockerNetworkInspect {
Options?: unknown;
}

interface BridgeTopology {
export interface BridgeTopology {
bridgeInterface: string;
gatewayIp: string;
prefixLength: number;
Expand Down Expand Up @@ -65,7 +65,7 @@ function parseBridgeCidr(value: string): { network: number; prefixLength: number
return (network & mask) >>> 0 === network ? { network, prefixLength } : undefined;
}

function parseBridgeTopology(stdout: string): BridgeTopology {
export function parseBridgeTopology(stdout: string): BridgeTopology {
let parsed: unknown;
try {
parsed = JSON.parse(stdout);
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/lib/fake-api-port-proxy.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import net from "node:net";

const target = process.env.NEMOCLAW_FAKE_API_PROXY_TARGET ?? "";
const portList = process.env.NEMOCLAW_FAKE_API_PROXY_PORTS ?? "";
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(target)) {
throw new Error("NEMOCLAW_FAKE_API_PROXY_TARGET must be a Docker container name");
}
const ports = portList === "8080" ? [8080] : portList === "8080,8081" ? [8080, 8081] : undefined;
if (!ports) throw new Error("NEMOCLAW_FAKE_API_PROXY_PORTS must be 8080 or 8080,8081");

for (const port of ports) {
const server = net.createServer((client) => {
const upstream = net.createConnection({ host: target, port });
const close = () => {
client.destroy();
upstream.destroy();
};
client.on("error", close);
upstream.on("error", close);
client.pipe(upstream);
upstream.pipe(client);
});
server.once("error", (error) => {
process.stderr.write(`fake API proxy listener ${port} failed: ${error.message}\n`);
process.exit(1);
});
server.listen(port, "0.0.0.0");
}
150 changes: 150 additions & 0 deletions test/e2e/lib/fake-api-port-readiness.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { isIP } from "node:net";
import { pathToFileURL } from "node:url";

const ATTEMPT_TIMEOUT_MS = 1_000;
const PORT_TRAFFIC_TIMEOUT_MS = 10_000;
const RETRY_DELAY_MS = 100;
const MAX_REPLY_BYTES = 8_192;
const PORT_TRAFFIC_PROBE = "nemoclaw_port_traffic_probe";
const PORT_TRAFFIC_REPLY = "nemoclaw_port_traffic_reply";

function requirePort(raw: string, name: string): number {
if (!/^[1-9][0-9]{0,4}$/u.test(raw)) throw new Error(`${name} must be a decimal port`);
const port = Number(raw);
if (port > 65_535) throw new Error(`${name} must be at most 65535`);
return port;
}

function urlHost(host: string): string {
return isIP(host) === 6 ? `[${host}]` : host;
}

async function probeHttp(host: string, port: number): Promise<void> {
const response = await fetch(`http://${urlHost(host)}:${port}/__nemoclaw_e2e_port_traffic`, {
signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS),
});
const reader = response.body?.getReader();
if (!reader) return;
let replyBytes = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) return;
replyBytes += value.byteLength;
if (replyBytes > MAX_REPLY_BYTES) {
await reader.cancel();
throw new Error("REST port traffic reply exceeded 8192 bytes");
}
}
}

function probeWebsocket(host: string, port: number): Promise<void> {
return new Promise((resolve, reject) => {
const socket = new WebSocket(`ws://${urlHost(host)}:${port}/socket-mode`);
let settled = false;
const finish = (error?: Error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
try {
socket.close();
} catch {
// Preserve the traffic-check result when the native client is not open.
}
if (error) reject(error);
else resolve();
};
const timer = setTimeout(() => {
finish(new Error("WebSocket port traffic timed out"));
}, ATTEMPT_TIMEOUT_MS);
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: PORT_TRAFFIC_PROBE }));
});
socket.addEventListener("message", (event) => {
if (typeof event.data !== "string") {
finish(new Error("WebSocket port traffic reply was not text"));
return;
}
if (Buffer.byteLength(event.data, "utf8") > MAX_REPLY_BYTES) {
finish(new Error("WebSocket port traffic reply exceeded 8192 bytes"));
return;
}
try {
const reply = JSON.parse(event.data);
if (reply?.type !== PORT_TRAFFIC_REPLY) {
finish(new Error("WebSocket port traffic reply was not recognized"));
return;
}
} catch {
finish(new Error("WebSocket port traffic reply was not valid JSON"));
return;
}
finish();
});
socket.addEventListener("error", () => {
finish(new Error("WebSocket port traffic connection failed"));
});
socket.addEventListener("close", () => {
finish(new Error("WebSocket port traffic closed before the reply"));
});
});
}

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function proveFakeApiPortTraffic(options: {
host: string;
restPort: number;
websocketPort?: number;
}): Promise<void> {
if (isIP(options.host) === 0) throw new Error("port traffic host must be an IP address");
if (!Number.isInteger(options.restPort) || options.restPort < 1 || options.restPort > 65_535) {
throw new Error("REST port must be an integer between 1 and 65535");
}
if (
options.websocketPort !== undefined &&
(!Number.isInteger(options.websocketPort) ||
options.websocketPort < 1 ||
options.websocketPort > 65_535)
) {
throw new Error("WebSocket port must be an integer between 1 and 65535");
}
const deadline = Date.now() + PORT_TRAFFIC_TIMEOUT_MS;
let lastError: string | undefined;
do {
try {
await probeHttp(options.host, options.restPort);
if (options.websocketPort !== undefined) {
await probeWebsocket(options.host, options.websocketPort);
}
return;
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
await sleep(RETRY_DELAY_MS);
} while (Date.now() < deadline);
throw new Error(`fake API port traffic check failed: ${lastError ?? "no reply"}`);
}

async function main(): Promise<void> {
const [host = "", restPortRaw = "", websocketPortRaw] = process.argv.slice(2);
await proveFakeApiPortTraffic({
host,
restPort: requirePort(restPortRaw, "REST port"),
...(websocketPortRaw === undefined
? {}
: { websocketPort: requirePort(websocketPortRaw, "WebSocket port") }),
});
process.stdout.write("fake API proxy ports carry traffic\n");
}

if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});
}
8 changes: 8 additions & 0 deletions test/e2e/lib/fake-slack-api.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const expectedAppToken = process.env.FAKE_SLACK_API_EXPECTED_APP_TOKEN || "";
const socketUserId = process.env.FAKE_SLACK_API_SOCKET_USER_ID || "U3730E2E";
const socketChannelId = process.env.FAKE_SLACK_API_SOCKET_CHANNEL_ID || "D3730E2E";
const socketTeamId = process.env.FAKE_SLACK_API_SOCKET_TEAM_ID || "T3730E2E";
const suppressPortTrafficReply = process.env.FAKE_SLACK_API_SUPPRESS_PORT_TRAFFIC_REPLY === "1";
const MAX_BODY_BYTES = 1024 * 1024;
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

Expand Down Expand Up @@ -298,6 +299,13 @@ function handleUpgrade(req, socket) {
tokenLooksPlaceholder: tokenLooksPlaceholder(text),
textRedacted: true,
});
if (messageType === "nemoclaw_port_traffic_probe") {
if (!suppressPortTrafficReply) {
socket.write(encodeServerText(JSON.stringify({ type: "nemoclaw_port_traffic_reply" })));
record({ event: "websocket-port-traffic-reply", path: pathname });
}
continue;
}
if (!sentEvent) {
sentEvent = true;
sendSocketModeEvent(socket);
Expand Down
Loading
Loading