Add new tests for shell bridge and broker to handle structured failures and invalid session handling

- Add tests for shell bridge to validate structured error reporting on spawn failures.

- Add broker tests to ensure proper rejection of malformed browser shell upgrades without leaking sensitive tokens.

- Update `.env.example` with `EDGE_PUBLIC_BROKER_URL` for public access configuration.
This commit is contained in:
Jeppe Bundgaard
2026-04-28 09:00:12 +02:00
parent e93d3f30d2
commit 2aded0812a
10 changed files with 2699 additions and 13 deletions
+1
View File
@@ -49,6 +49,7 @@ ECONOMIC_API_APP_SECRET_TOKEN=
# Edge broker defaults for shell relay and gateway dispatch.
EDGE_BROKER_URL=http://edge-broker:4300
EDGE_PUBLIC_BROKER_URL=http://localhost/api/edge-broker
EDGE_BROKER_SHARED_SECRET=truckwash-edge-dev
# Redis credentials
+5
View File
@@ -70,6 +70,11 @@ When updating `.env` values used by PHP containers (for example e-conomic tokens
docker compose up -d --force-recreate php1 php2 php3 php4 php5 php-cron
```
#### Edge Broker Public URL
Set `EDGE_PUBLIC_BROKER_URL` to the public route that serves the edge broker, including the path prefix handled by the proxy. Local Traefik uses `http://localhost/api/edge-broker`; production routes use the public broker prefix, for example `https://api.truckwash.dk/edge-broker`.
The browser terminal connects to the exact advertised `EDGE_PUBLIC_BROKER_URL` plus `/ws/browser-shell`. That URL must be routable through the proxy to the edge-broker service. Do not rely on derived `/api/edge-broker` fallback paths outside the local Traefik setup.
## Testing
The project now uses [Pest](https://pestphp.com/) as the primary test runner in `services/nginx/app`.
File diff suppressed because it is too large Load Diff
+33
View File
@@ -625,6 +625,39 @@ test("shell bridge proxies PTY output, input, resize, close, and dispose events"
assert.ok(messages.some((message) => message.type === "SHELL_EXIT" && message.code === 0));
});
test("shell bridge reports structured spawn failures", async () => {
const messages = [];
const shell = createShellBridge(
(message) => messages.push(message),
{
createPtyProcess: async () => {
throw new Error("node-pty unavailable");
},
}
);
await shell.open({ sessionId: "spawn-failure-shell" });
assert.ok(
messages.some(
(message) =>
message.type === "SHELL_OUTPUT" &&
message.sessionId === "spawn-failure-shell" &&
/Failed to start root shell: node-pty unavailable/.test(message.data)
)
);
assert.ok(
messages.some(
(message) =>
message.type === "SHELL_EXIT" &&
message.sessionId === "spawn-failure-shell" &&
message.code === 1 &&
message.reason === "shell_spawn_failed" &&
message.message === "node-pty unavailable"
)
);
});
test("startAgent reports API polling metadata, executes polled commands, and uploads shell events", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-"));
const configPath = path.join(tempDir, "config.json");
+104
View File
@@ -1,5 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import net from "node:net";
import WebSocket from "ws";
import { createBrokerServer } from "../server.mjs";
@@ -18,6 +19,32 @@ function waitForClose(socket) {
});
}
function rawUpgradeRequest(port, path) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host: "127.0.0.1", port }, () => {
socket.write(
[
`GET ${path} HTTP/1.1`,
`Host: 127.0.0.1:${port}`,
"Connection: Upgrade",
"Upgrade: websocket",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version: 13",
"",
"",
].join("\r\n")
);
});
let response = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
response += chunk;
});
socket.on("end", () => resolve(response));
socket.on("error", reject);
});
}
async function waitFor(predicate, { timeoutMs = 1000, intervalMs = 10, description = "condition" } = {}) {
const deadline = Date.now() + timeoutMs;
@@ -192,6 +219,83 @@ test("broker closes browser shell sessions immediately when no agent is connecte
await broker.close();
});
test("broker rejects browser shell upgrades without a token using HTTP diagnostics", async () => {
const broker = createBrokerServer({ authMode: "stub" });
const address = await broker.listen(0);
const port = address.port;
const response = await rawUpgradeRequest(port, "/ws/browser-shell");
assert.match(response, /^HTTP\/1\.1 400 Bad Request/m);
assert.match(response, /"error_code":"shell_session_token_missing"/);
assert.match(response, /"message":"Missing shell session token\."/);
await broker.close();
});
test("broker rejects invalid browser shell upgrades without leaking the token", async () => {
const broker = createBrokerServer({
authMode: "stub",
validateShellSession: async () => {
const error = new Error("Shell session expired");
error.status = 401;
error.code = "shell_session_expired";
throw error;
},
});
const address = await broker.listen(0);
const port = address.port;
const rawToken = "session-token-secret";
const response = await rawUpgradeRequest(port, `/ws/browser-shell?token=${rawToken}`);
assert.match(response, /^HTTP\/1\.1 401 Unauthorized/m);
assert.match(response, /"error_code":"shell_session_expired"/);
assert.doesNotMatch(response, new RegExp(rawToken));
await broker.close();
});
test("broker closes browser shell sessions when the agent never reports shell opened", async () => {
const closedSessions = [];
const broker = createBrokerServer({
authMode: "stub",
shellOpenTimeoutMs: 30,
validateShellSession: async () => ({ id: "shell-timeout", gateway_id: "701", reason: "diagnostic" }),
closeShellSession: async (_id, _token, transcript, reason, details) => {
closedSessions.push({ transcript, reason, details });
},
});
const address = await broker.listen(0);
const port = address.port;
const agent = new WebSocket(`ws://127.0.0.1:${port}/ws/agent?gatewayId=701&token=agent-token`);
const agentMessages = collectMessages(agent);
await new Promise((resolve) => agent.once("open", resolve));
const browser = new WebSocket(`ws://127.0.0.1:${port}/ws/browser-shell?token=session-token`);
const browserMessages = collectMessages(browser);
await new Promise((resolve) => browser.once("open", resolve));
await waitFor(
() =>
agentMessages.some(
(message) => message.type === "OPEN_ROOT_SHELL" && message.payload.sessionId === "shell-timeout"
),
{ description: "agent shell open request before timeout" }
);
await waitForClose(browser);
await waitFor(() => closedSessions.length === 1, { description: "timeout shell session close callback" });
assert.ok(browserMessages.some((message) => message.type === "closed" && message.reason === "shell_open_timeout"));
assert.equal(closedSessions[0].reason, "shell_open_timeout");
assert.equal(closedSessions[0].details.stage, "shell_open");
assert.equal(closedSessions[0].details.code, 1011);
agent.terminate();
await broker.close();
});
test("broker closes browser shell sessions when the agent disconnects before shell open", async () => {
const closedSessions = [];
const broker = createBrokerServer({
File diff suppressed because one or more lines are too long
@@ -11868,3 +11868,255 @@
[Thu Apr 23 19:30:02 2026] 127.0.0.1:38996 Closing
[Thu Apr 23 19:30:10 2026] 127.0.0.1:59878 Accepted
[Thu Apr 23 19:30:16 2026] 127.0.0.1:59878 Closing
[Mon Apr 27 15:43:00 2026] PHP 8.2.15 Development Server (http://127.0.0.1:39017) started
[Mon Apr 27 15:43:00 2026] 127.0.0.1:43538 Accepted
[Mon Apr 27 15:43:02 2026] 127.0.0.1:43538 Closing
[Mon Apr 27 15:43:02 2026] 127.0.0.1:59780 Accepted
[Mon Apr 27 15:43:09 2026] 127.0.0.1:59780 Closing
[Mon Apr 27 15:43:09 2026] 127.0.0.1:59790 Accepted
[Mon Apr 27 15:43:21 2026] 127.0.0.1:59790 Closing
[Mon Apr 27 15:43:21 2026] 127.0.0.1:55448 Accepted
[Mon Apr 27 15:43:37 2026] 127.0.0.1:55448 Closing
[Mon Apr 27 15:43:37 2026] 127.0.0.1:39854 Accepted
[Mon Apr 27 15:43:49 2026] 127.0.0.1:39854 Closing
[Mon Apr 27 15:43:49 2026] 127.0.0.1:40450 Accepted
[Mon Apr 27 15:44:00 2026] 127.0.0.1:40450 Closing
[Mon Apr 27 15:44:00 2026] 127.0.0.1:37108 Accepted
[Mon Apr 27 15:44:04 2026] 127.0.0.1:37108 Closing
[Mon Apr 27 15:44:04 2026] 127.0.0.1:51378 Accepted
[Mon Apr 27 15:44:20 2026] 127.0.0.1:51378 Closing
[Mon Apr 27 15:44:20 2026] 127.0.0.1:57924 Accepted
[Mon Apr 27 15:44:26 2026] 127.0.0.1:57924 Closing
[Mon Apr 27 15:44:26 2026] 127.0.0.1:36924 Accepted
[Mon Apr 27 15:44:33 2026] 127.0.0.1:36924 Closing
[Mon Apr 27 15:44:33 2026] 127.0.0.1:41488 Accepted
[Mon Apr 27 15:44:50 2026] 127.0.0.1:41488 Closing
[Mon Apr 27 15:44:50 2026] 127.0.0.1:42138 Accepted
[Mon Apr 27 15:45:06 2026] 127.0.0.1:42138 Closing
[Mon Apr 27 15:45:06 2026] 127.0.0.1:56450 Accepted
[Mon Apr 27 15:45:24 2026] 127.0.0.1:56450 Closing
[Mon Apr 27 15:45:31 2026] 127.0.0.1:50392 Accepted
[Mon Apr 27 15:45:45 2026] 127.0.0.1:50392 Closing
[Mon Apr 27 15:45:51 2026] 127.0.0.1:44926 Accepted
[Mon Apr 27 15:46:21 2026] 127.0.0.1:44926 Closing
[Mon Apr 27 15:46:21 2026] 127.0.0.1:34068 Accepted
[Mon Apr 27 15:46:29 2026] PHP 8.2.15 Development Server (http://127.0.0.1:39779) started
[Mon Apr 27 15:46:29 2026] 127.0.0.1:36812 Accepted
[Mon Apr 27 15:46:30 2026] 127.0.0.1:34068 Closing
[Mon Apr 27 15:46:30 2026] 127.0.0.1:45262 Accepted
[Mon Apr 27 15:46:31 2026] 127.0.0.1:36812 Closing
[Mon Apr 27 15:46:31 2026] 127.0.0.1:53156 Accepted
[Mon Apr 27 15:46:39 2026] 127.0.0.1:45262 Closing
[Mon Apr 27 15:46:39 2026] 127.0.0.1:45266 Accepted
[Mon Apr 27 15:46:44 2026] 127.0.0.1:53156 Closing
[Mon Apr 27 15:46:52 2026] 127.0.0.1:45266 Closing
[Mon Apr 27 15:46:52 2026] 127.0.0.1:37766 Accepted
[Mon Apr 27 15:47:10 2026] PHP 8.2.15 Development Server (http://127.0.0.1:39511) started
[Mon Apr 27 15:47:11 2026] 127.0.0.1:41892 Accepted
[Mon Apr 27 15:47:14 2026] 127.0.0.1:41892 Closing
[Mon Apr 27 15:47:14 2026] 127.0.0.1:41904 Accepted
[Mon Apr 27 15:47:17 2026] 127.0.0.1:37766 Closing
[Mon Apr 27 15:47:17 2026] 127.0.0.1:36058 Accepted
[Mon Apr 27 15:47:19 2026] 127.0.0.1:41904 Closing
[Mon Apr 27 15:47:19 2026] 127.0.0.1:36924 Accepted
[Mon Apr 27 15:47:26 2026] 127.0.0.1:36058 Closing
[Mon Apr 27 15:47:26 2026] 127.0.0.1:51896 Accepted
[Mon Apr 27 15:47:31 2026] 127.0.0.1:36924 Closing
[Mon Apr 27 15:47:31 2026] 127.0.0.1:34276 Accepted
[Mon Apr 27 15:47:46 2026] 127.0.0.1:34276 Closing
[Mon Apr 27 15:47:46 2026] 127.0.0.1:37246 Accepted
[Mon Apr 27 15:47:57 2026] 127.0.0.1:51896 Closing
[Mon Apr 27 15:47:59 2026] 127.0.0.1:37246 Closing
[Mon Apr 27 15:47:59 2026] 127.0.0.1:36472 Accepted
[Mon Apr 27 15:48:03 2026] 127.0.0.1:33350 Accepted
[Mon Apr 27 15:48:07 2026] 127.0.0.1:36472 Closing
[Mon Apr 27 15:48:07 2026] 127.0.0.1:56178 Accepted
[Mon Apr 27 15:48:07 2026] 127.0.0.1:33350 Closing
[Mon Apr 27 15:48:10 2026] 127.0.0.1:40096 Accepted
[Mon Apr 27 15:48:13 2026] 127.0.0.1:56178 Closing
[Mon Apr 27 15:48:13 2026] 127.0.0.1:55944 Accepted
[Mon Apr 27 15:48:19 2026] 127.0.0.1:40096 Closing
[Mon Apr 27 15:48:19 2026] 127.0.0.1:40104 Accepted
[Mon Apr 27 15:48:28 2026] 127.0.0.1:40104 Closing
[Mon Apr 27 15:48:28 2026] 127.0.0.1:55654 Accepted
[Mon Apr 27 15:48:30 2026] 127.0.0.1:55944 Closing
[Mon Apr 27 15:48:30 2026] 127.0.0.1:41444 Accepted
[Mon Apr 27 15:48:33 2026] 127.0.0.1:55654 Closing
[Mon Apr 27 15:48:33 2026] 127.0.0.1:54932 Accepted
[Mon Apr 27 15:48:36 2026] 127.0.0.1:41444 Closing
[Mon Apr 27 15:48:36 2026] 127.0.0.1:41450 Accepted
[Mon Apr 27 15:48:39 2026] 127.0.0.1:54932 Closing
[Mon Apr 27 15:48:39 2026] 127.0.0.1:54942 Accepted
[Mon Apr 27 15:48:46 2026] 127.0.0.1:41450 Closing
[Mon Apr 27 15:48:46 2026] 127.0.0.1:42874 Accepted
[Mon Apr 27 15:48:46 2026] 127.0.0.1:54942 Closing
[Mon Apr 27 15:48:46 2026] 127.0.0.1:49554 Accepted
[Mon Apr 27 15:49:03 2026] 127.0.0.1:49554 Closing
[Mon Apr 27 15:49:03 2026] 127.0.0.1:51798 Accepted
[Mon Apr 27 15:49:04 2026] 127.0.0.1:42874 Closing
[Mon Apr 27 15:49:04 2026] 127.0.0.1:56176 Accepted
[Mon Apr 27 15:49:11 2026] 127.0.0.1:51798 Closing
[Mon Apr 27 15:49:11 2026] 127.0.0.1:47548 Accepted
[Mon Apr 27 15:49:22 2026] 127.0.0.1:56176 Closing
[Mon Apr 27 15:49:22 2026] 127.0.0.1:52542 Accepted
[Mon Apr 27 15:49:24 2026] 127.0.0.1:47548 Closing
[Mon Apr 27 15:49:24 2026] 127.0.0.1:39848 Accepted
[Mon Apr 27 15:49:36 2026] 127.0.0.1:39848 Closing
[Mon Apr 27 15:49:43 2026] 127.0.0.1:60636 Accepted
[Mon Apr 27 15:49:43 2026] 127.0.0.1:52542 Closing
[Mon Apr 27 15:49:50 2026] 127.0.0.1:43864 Accepted
[Mon Apr 27 15:50:03 2026] 127.0.0.1:43864 Closing
[Mon Apr 27 15:50:07 2026] 127.0.0.1:60636 Closing
[Mon Apr 27 15:50:07 2026] 127.0.0.1:48092 Accepted
[Mon Apr 27 15:50:11 2026] 127.0.0.1:38378 Accepted
[Mon Apr 27 15:50:26 2026] 127.0.0.1:48092 Closing
[Mon Apr 27 15:50:26 2026] 127.0.0.1:51390 Accepted
[Mon Apr 27 15:50:41 2026] 127.0.0.1:38378 Closing
[Mon Apr 27 15:50:49 2026] 127.0.0.1:38158 Accepted
[Mon Apr 27 15:50:54 2026] 127.0.0.1:51390 Closing
[Mon Apr 27 15:50:54 2026] 127.0.0.1:33842 Accepted
[Mon Apr 27 15:50:56 2026] 127.0.0.1:38158 Closing
[Mon Apr 27 15:51:00 2026] 127.0.0.1:36832 Accepted
[Mon Apr 27 15:51:13 2026] 127.0.0.1:36832 Closing
[Mon Apr 27 15:51:13 2026] 127.0.0.1:39172 Accepted
[Mon Apr 27 15:51:19 2026] 127.0.0.1:33842 Closing
[Mon Apr 27 15:51:19 2026] 127.0.0.1:47144 Accepted
[Mon Apr 27 15:51:25 2026] 127.0.0.1:39172 Closing
[Mon Apr 27 15:51:25 2026] 127.0.0.1:33908 Accepted
[Mon Apr 27 15:51:32 2026] 127.0.0.1:47144 Closing
[Mon Apr 27 15:51:32 2026] 127.0.0.1:35586 Accepted
[Mon Apr 27 15:51:39 2026] 127.0.0.1:33908 Closing
[Mon Apr 27 15:51:39 2026] 127.0.0.1:49634 Accepted
[Mon Apr 27 15:51:49 2026] 127.0.0.1:49634 Closing
[Mon Apr 27 15:51:49 2026] 127.0.0.1:35846 Accepted
[Mon Apr 27 15:52:02 2026] 127.0.0.1:35846 Closing
[Mon Apr 27 15:52:02 2026] 127.0.0.1:40002 Accepted
[Mon Apr 27 15:52:03 2026] 127.0.0.1:35586 Closing
[Mon Apr 27 15:52:09 2026] 127.0.0.1:38974 Accepted
[Mon Apr 27 15:52:22 2026] 127.0.0.1:40002 Closing
[Mon Apr 27 15:52:22 2026] 127.0.0.1:39160 Accepted
[Mon Apr 27 15:52:22 2026] 127.0.0.1:38974 Closing
[Mon Apr 27 15:52:23 2026] 127.0.0.1:50508 Accepted
[Mon Apr 27 15:52:31 2026] 127.0.0.1:50508 Closing
[Mon Apr 27 15:52:31 2026] 127.0.0.1:34868 Accepted
[Mon Apr 27 15:52:33 2026] 127.0.0.1:39160 Closing
[Mon Apr 27 15:52:33 2026] 127.0.0.1:51124 Accepted
[Mon Apr 27 15:52:45 2026] 127.0.0.1:34868 Closing
[Mon Apr 27 15:52:51 2026] 127.0.0.1:51124 Closing
[Mon Apr 27 15:52:51 2026] 127.0.0.1:38142 Accepted
[Mon Apr 27 15:52:58 2026] 127.0.0.1:40286 Accepted
[Mon Apr 27 15:53:08 2026] 127.0.0.1:38142 Closing
[Mon Apr 27 15:53:15 2026] 127.0.0.1:47064 Accepted
[Mon Apr 27 15:53:43 2026] 127.0.0.1:47064 Closing
[Mon Apr 27 15:53:43 2026] 127.0.0.1:40368 Accepted
[Mon Apr 27 15:54:10 2026] 127.0.0.1:40286 Closing
[Mon Apr 27 15:54:12 2026] 127.0.0.1:40368 Closing
[Mon Apr 27 15:54:12 2026] 127.0.0.1:59302 Accepted
[Mon Apr 27 15:54:44 2026] 127.0.0.1:59302 Closing
[Mon Apr 27 15:54:51 2026] 127.0.0.1:55188 Accepted
[Mon Apr 27 15:55:05 2026] 127.0.0.1:55188 Closing
[Mon Apr 27 15:55:06 2026] 127.0.0.1:54998 Accepted
[Mon Apr 27 15:55:14 2026] 127.0.0.1:54998 Closing
[Mon Apr 27 15:55:14 2026] 127.0.0.1:55388 Accepted
[Mon Apr 27 15:55:27 2026] 127.0.0.1:55388 Closing
[Mon Apr 27 15:55:38 2026] 127.0.0.1:58096 Accepted
[Mon Apr 27 15:56:40 2026] 127.0.0.1:58096 Closing
[Mon Apr 27 15:57:41 2026] PHP 8.2.15 Development Server (http://127.0.0.1:43345) started
[Mon Apr 27 15:57:41 2026] PHP 8.2.15 Development Server (http://127.0.0.1:45337) started
[Mon Apr 27 15:57:41 2026] 127.0.0.1:54434 Accepted
[Mon Apr 27 15:57:41 2026] 127.0.0.1:42766 Accepted
[Mon Apr 27 15:57:46 2026] 127.0.0.1:42766 Closing
[Mon Apr 27 15:57:46 2026] 127.0.0.1:54434 Closing
[Mon Apr 27 15:57:46 2026] 127.0.0.1:42782 Accepted
[Mon Apr 27 15:57:46 2026] 127.0.0.1:54438 Accepted
[Mon Apr 27 15:57:54 2026] 127.0.0.1:54438 Closing
[Mon Apr 27 15:57:54 2026] 127.0.0.1:43210 Accepted
[Mon Apr 27 15:58:08 2026] 127.0.0.1:43210 Closing
[Mon Apr 27 15:58:08 2026] 127.0.0.1:37286 Accepted
[Mon Apr 27 15:58:10 2026] 127.0.0.1:42782 Closing
[Mon Apr 27 15:58:10 2026] 127.0.0.1:57164 Accepted
[Mon Apr 27 15:58:25 2026] 127.0.0.1:37286 Closing
[Mon Apr 27 15:58:25 2026] 127.0.0.1:33692 Accepted
[Mon Apr 27 15:58:31 2026] 127.0.0.1:57164 Closing
[Mon Apr 27 15:58:31 2026] 127.0.0.1:41728 Accepted
[Mon Apr 27 15:58:41 2026] 127.0.0.1:33692 Closing
[Mon Apr 27 15:58:41 2026] 127.0.0.1:44756 Accepted
[Mon Apr 27 15:58:52 2026] 127.0.0.1:44756 Closing
[Mon Apr 27 15:58:52 2026] 127.0.0.1:41988 Accepted
[Mon Apr 27 15:58:59 2026] 127.0.0.1:41988 Closing
[Mon Apr 27 15:58:59 2026] 127.0.0.1:34238 Accepted
[Mon Apr 27 15:59:01 2026] 127.0.0.1:41728 Closing
[Mon Apr 27 15:59:01 2026] 127.0.0.1:41632 Accepted
[Mon Apr 27 15:59:10 2026] 127.0.0.1:41632 Closing
[Mon Apr 27 15:59:10 2026] 127.0.0.1:56798 Accepted
[Mon Apr 27 15:59:17 2026] 127.0.0.1:34238 Closing
[Mon Apr 27 15:59:17 2026] 127.0.0.1:51968 Accepted
[Mon Apr 27 15:59:24 2026] 127.0.0.1:51968 Closing
[Mon Apr 27 15:59:24 2026] 127.0.0.1:38554 Accepted
[Mon Apr 27 15:59:28 2026] 127.0.0.1:56798 Closing
[Mon Apr 27 15:59:28 2026] 127.0.0.1:37742 Accepted
[Mon Apr 27 15:59:33 2026] 127.0.0.1:38554 Closing
[Mon Apr 27 15:59:33 2026] 127.0.0.1:60924 Accepted
[Mon Apr 27 15:59:52 2026] 127.0.0.1:60924 Closing
[Mon Apr 27 15:59:52 2026] 127.0.0.1:43252 Accepted
[Mon Apr 27 15:59:55 2026] 127.0.0.1:37742 Closing
[Mon Apr 27 15:59:55 2026] 127.0.0.1:43598 Accepted
[Mon Apr 27 16:00:08 2026] 127.0.0.1:43598 Closing
[Mon Apr 27 16:00:08 2026] 127.0.0.1:45400 Accepted
[Mon Apr 27 16:00:15 2026] 127.0.0.1:43252 Closing
[Mon Apr 27 16:00:15 2026] 127.0.0.1:33680 Accepted
[Mon Apr 27 16:00:28 2026] 127.0.0.1:45400 Closing
[Mon Apr 27 16:00:28 2026] 127.0.0.1:58188 Accepted
[Mon Apr 27 16:00:38 2026] 127.0.0.1:33680 Closing
[Mon Apr 27 16:00:43 2026] 127.0.0.1:58188 Closing
[Mon Apr 27 16:00:43 2026] 127.0.0.1:52096 Accepted
[Mon Apr 27 16:01:17 2026] 127.0.0.1:52096 Closing
[Mon Apr 27 16:02:10 2026] PHP 8.2.15 Development Server (http://127.0.0.1:37323) started
[Mon Apr 27 16:02:10 2026] 127.0.0.1:35510 Accepted
[Mon Apr 27 16:02:14 2026] 127.0.0.1:35510 Closing
[Mon Apr 27 16:02:14 2026] 127.0.0.1:35512 Accepted
[Mon Apr 27 16:02:34 2026] 127.0.0.1:35512 Closing
[Mon Apr 27 16:02:35 2026] 127.0.0.1:50544 Accepted
[Mon Apr 27 16:02:54 2026] 127.0.0.1:50544 Closing
[Mon Apr 27 16:02:54 2026] 127.0.0.1:34284 Accepted
[Mon Apr 27 16:03:23 2026] 127.0.0.1:34284 Closing
[Mon Apr 27 16:03:23 2026] 127.0.0.1:58504 Accepted
[Mon Apr 27 16:03:35 2026] 127.0.0.1:58504 Closing
[Mon Apr 27 16:03:35 2026] 127.0.0.1:57200 Accepted
[Mon Apr 27 16:03:55 2026] 127.0.0.1:57200 Closing
[Mon Apr 27 16:03:56 2026] 127.0.0.1:37528 Accepted
[Mon Apr 27 16:04:24 2026] 127.0.0.1:37528 Closing
[Mon Apr 27 16:04:24 2026] 127.0.0.1:41972 Accepted
[Mon Apr 27 16:04:48 2026] 127.0.0.1:41972 Closing
[Mon Apr 27 16:04:48 2026] 127.0.0.1:40764 Accepted
[Mon Apr 27 16:05:10 2026] 127.0.0.1:40764 Closing
[Mon Apr 27 16:05:10 2026] 127.0.0.1:51052 Accepted
[Mon Apr 27 16:05:25 2026] 127.0.0.1:51052 Closing
[Mon Apr 27 16:05:25 2026] 127.0.0.1:49526 Accepted
[Mon Apr 27 16:05:58 2026] 127.0.0.1:49526 Closing
[Mon Apr 27 16:05:58 2026] 127.0.0.1:54380 Accepted
[Mon Apr 27 16:06:10 2026] 127.0.0.1:54380 Closing
[Mon Apr 27 16:06:10 2026] 127.0.0.1:38446 Accepted
[Mon Apr 27 16:06:33 2026] 127.0.0.1:38446 Closing
[Mon Apr 27 16:07:01 2026] PHP 8.2.15 Development Server (http://127.0.0.1:46263) started
[Mon Apr 27 16:07:01 2026] PHP 8.2.15 Development Server (http://127.0.0.1:44107) started
[Mon Apr 27 16:07:01 2026] 127.0.0.1:57266 Accepted
[Mon Apr 27 16:07:01 2026] 127.0.0.1:47356 Accepted
[Mon Apr 27 16:07:05 2026] 127.0.0.1:57266 Closing
[Mon Apr 27 16:07:05 2026] 127.0.0.1:57282 Accepted
[Mon Apr 27 16:07:05 2026] 127.0.0.1:47356 Closing
[Mon Apr 27 16:07:05 2026] 127.0.0.1:47362 Accepted
[Mon Apr 27 16:07:21 2026] 127.0.0.1:47362 Closing
[Mon Apr 27 16:07:37 2026] 127.0.0.1:57282 Closing
[Mon Apr 27 16:07:38 2026] 127.0.0.1:41372 Accepted
[Mon Apr 27 16:07:53 2026] 127.0.0.1:41372 Closing
[Mon Apr 27 16:07:53 2026] 127.0.0.1:39536 Accepted
[Mon Apr 27 16:08:09 2026] 127.0.0.1:39536 Closing
[Mon Apr 27 16:08:09 2026] 127.0.0.1:33514 Accepted
[Mon Apr 27 16:08:28 2026] 127.0.0.1:33514 Closing
[Mon Apr 27 16:08:28 2026] 127.0.0.1:59450 Accepted
[Mon Apr 27 16:08:58 2026] 127.0.0.1:59450 Closing
[Mon Apr 27 16:08:58 2026] 127.0.0.1:40070 Accepted
[Mon Apr 27 16:09:11 2026] 127.0.0.1:40070 Closing
[Mon Apr 27 16:09:12 2026] 127.0.0.1:41964 Accepted
[Mon Apr 27 16:09:54 2026] 127.0.0.1:41964 Closing
@@ -181,6 +181,7 @@ it('validates broker sessions and ingests presence, telemetry, logs, and shell l
'message' => 'Shell exited cleanly.',
'code' => 0,
'stage' => 'shell_active',
'ws_url' => 'wss://broker.example.test/ws/browser-shell?token=raw-shell-token',
],
edge_test_broker_headers()
);
@@ -201,7 +202,9 @@ it('validates broker sessions and ingests presence, telemetry, logs, and shell l
->and($closedShell->data()['metadata']['close_code'] ?? null)
->toBe(0)
->and($closedShell->data()['metadata']['close_stage'] ?? null)
->toBe('shell_active');
->toBe('shell_active')
->and($closedShell->data()['metadata']['close_diagnostics']['ws_url'] ?? null)
->toBe('wss://broker.example.test/ws/browser-shell?token=***');
$logsPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/logs', $session['headers']);
@@ -270,6 +270,20 @@ it('manages edge gateway metadata, bindings, operations, sessions, rotation, cut
->toHaveKey('token')
->toHaveKey('ws_url');
$brokerPresence = api_client()->post(
'/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/presence',
[
'status' => 'connected',
'connection_id' => 'operator-shell-broker-1',
],
edge_test_broker_headers()
);
$brokerPresence
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$shellSession = api_client()->post(
'/edge-gateways/' . (int)$gateway['id'] . '/shell-sessions',
[
@@ -290,7 +304,9 @@ it('manages edge gateway metadata, bindings, operations, sessions, rotation, cut
->toHaveKey('token')
->toHaveKey('session')
->and($shellSession->data()['session']['reason'] ?? null)
->toBe('Operator smoke session');
->toBe('Operator smoke session')
->and($shellSession->data()['diagnostics']['broker_presence']['connection_id'] ?? null)
->toBe('operator-shell-broker-1');
$rotateResponse = api_client()->post(
'/edge-gateways/' . (int)$gateway['id'] . '/rotate-credentials',
@@ -169,6 +169,14 @@ it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle
->and($commandResult['job']['status'] ?? null)
->toBe('COMPLETED');
$context['manager']->recordBrokerPresence(
$gatewayId,
'connected',
'broker-connection-1',
null,
['transport' => 'ws']
);
$shellSession = $context['manager']->createShellSession(
$gatewayId,
(int)$user['id'],
@@ -179,6 +187,8 @@ it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle
);
$shellToken = (string)($shellSession['token'] ?? '');
expect($shellToken)->not->toBe('');
expect($shellSession['diagnostics']['broker_presence']['connection_id'] ?? null)
->toBe('broker-connection-1');
$validatedShell = $context['manager']->validateShellSessionToken($shellToken);
expect($validatedShell)->toHaveKey('status', 'PENDING');
@@ -189,12 +199,21 @@ it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle
$closedShell = $context['manager']->closeShellSessionByToken(
$shellToken,
"edge-shell-output\n",
'agent_exit'
'agent_exit',
[
'message' => 'Integration shell completed.',
'code' => 0,
'stage' => 'shell_active',
]
);
expect($closedShell)
->toHaveKey('status', 'COMPLETED')
->and($closedShell['transcript'] ?? null)
->toBe("edge-shell-output\n");
->toBe("edge-shell-output\n")
->and($closedShell['metadata']['close_reason'] ?? null)
->toBe('agent_exit')
->and($closedShell['metadata']['close_stage'] ?? null)
->toBe('shell_active');
$context['manager']->appendGatewayLogEntry(
$gatewayId,
@@ -205,14 +224,6 @@ it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle
['source' => 'integration']
);
$context['manager']->recordBrokerPresence(
$gatewayId,
'connected',
'broker-connection-1',
null,
['transport' => 'ws']
);
$context['manager']->recordTelemetryFromBroker($gatewayId, [
'status' => 'ONLINE',
'metadata' => [