Add unit tests for invoicing, orders normalization, gateway commands, and department complaints. Update schema bootstraps and improve agent command execution logic.
This commit is contained in:
@@ -9,8 +9,8 @@ import {
|
||||
claimIfNeeded,
|
||||
createShellBridge,
|
||||
getRelayStatus,
|
||||
startAgent,
|
||||
setRelayState,
|
||||
startAgent,
|
||||
} from "../dist/agent.mjs";
|
||||
|
||||
test("claimIfNeeded persists claimed gateway credentials", async () => {
|
||||
@@ -81,27 +81,90 @@ test("relay status and switch commands support both Shelly RPC and legacy endpoi
|
||||
assert.equal(switched.on, false);
|
||||
});
|
||||
|
||||
test("shell bridge streams child process output", async () => {
|
||||
test("shell bridge proxies PTY output, input, resize, and close events", async () => {
|
||||
const messages = [];
|
||||
const shell = createShellBridge((message) => messages.push(message));
|
||||
const createdPtys = [];
|
||||
|
||||
shell.open({
|
||||
const shell = createShellBridge(
|
||||
(message) => messages.push(message),
|
||||
{
|
||||
createPtyProcess: async (options) => {
|
||||
const listeners = {
|
||||
data: null,
|
||||
exit: null,
|
||||
};
|
||||
|
||||
const pty = {
|
||||
options,
|
||||
writes: [],
|
||||
resizes: [],
|
||||
killed: false,
|
||||
onData(callback) {
|
||||
listeners.data = callback;
|
||||
return {
|
||||
dispose() {
|
||||
listeners.data = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
onExit(callback) {
|
||||
listeners.exit = callback;
|
||||
return {
|
||||
dispose() {
|
||||
listeners.exit = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
write(data) {
|
||||
this.writes.push(data);
|
||||
},
|
||||
resize(cols, rows) {
|
||||
this.resizes.push({ cols, rows });
|
||||
},
|
||||
kill() {
|
||||
this.killed = true;
|
||||
listeners.exit?.({ exitCode: 0 });
|
||||
},
|
||||
emitData(data) {
|
||||
listeners.data?.(data);
|
||||
},
|
||||
};
|
||||
|
||||
createdPtys.push(pty);
|
||||
return pty;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await shell.open({
|
||||
sessionId: "test-shell",
|
||||
shellCommand: process.execPath,
|
||||
shellArgs: ["-e", "process.stdin.on('data', (d) => process.stdout.write(d))"],
|
||||
shellCommand: "/bin/bash",
|
||||
shellArgs: ["-l"],
|
||||
cols: 90,
|
||||
rows: 24,
|
||||
cwd: "/tmp",
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
shell.input({ sessionId: "test-shell", data: "hello\n" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
createdPtys[0].emitData("root@pi:~# ");
|
||||
shell.input({ sessionId: "test-shell", data: "ls\r" });
|
||||
shell.resize({ sessionId: "test-shell", cols: 120, rows: 40 });
|
||||
shell.close({ sessionId: "test-shell" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
assert.ok(messages.some((message) => message.type === "SHELL_OPENED"));
|
||||
assert.ok(messages.some((message) => message.type === "SHELL_OUTPUT" && message.data.includes("hello")));
|
||||
assert.equal(createdPtys.length, 1);
|
||||
assert.equal(createdPtys[0].options.command, "/bin/bash");
|
||||
assert.deepEqual(createdPtys[0].options.args, ["-l"]);
|
||||
assert.equal(createdPtys[0].options.cols, 90);
|
||||
assert.equal(createdPtys[0].options.rows, 24);
|
||||
assert.equal(createdPtys[0].options.cwd, "/tmp");
|
||||
assert.deepEqual(createdPtys[0].writes, ["ls\r"]);
|
||||
assert.deepEqual(createdPtys[0].resizes, [{ cols: 120, rows: 40 }]);
|
||||
assert.equal(createdPtys[0].killed, true);
|
||||
assert.ok(messages.some((message) => message.type === "SHELL_OPENED" && message.sessionId === "test-shell"));
|
||||
assert.ok(messages.some((message) => message.type === "SHELL_OUTPUT" && message.data.includes("root@pi")));
|
||||
assert.ok(messages.some((message) => message.type === "SHELL_EXIT" && message.code === 0));
|
||||
});
|
||||
|
||||
test("startAgent retries the broker tunnel and reports degraded heartbeats while disconnected", async () => {
|
||||
test("startAgent reports broker state via metadata and executes polled commands", async () => {
|
||||
class FakeSocket extends EventEmitter {
|
||||
constructor(url) {
|
||||
super();
|
||||
@@ -129,21 +192,85 @@ test("startAgent retries the broker tunnel and reports degraded heartbeats while
|
||||
agentToken: "agent-token",
|
||||
heartbeatIntervalSeconds: 60,
|
||||
reconnectDelayMs: 10,
|
||||
commandPollTimeoutSeconds: 0,
|
||||
commandPollRetryDelayMs: 5,
|
||||
}));
|
||||
|
||||
const heartbeats = [];
|
||||
const fakeFetch = async (url, options = {}) => {
|
||||
heartbeats.push({
|
||||
url,
|
||||
body: JSON.parse(options.body ?? "{}"),
|
||||
});
|
||||
const resultPosts = [];
|
||||
let polledCommandDelivered = false;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return { data: { ok: true } };
|
||||
},
|
||||
};
|
||||
const fakeFetch = async (url, options = {}) => {
|
||||
const body = options.body ? JSON.parse(options.body) : {};
|
||||
|
||||
if (String(url).endsWith("/heartbeat")) {
|
||||
heartbeats.push({
|
||||
url,
|
||||
body,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return { data: { ok: true } };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (String(url).endsWith("/commands/poll")) {
|
||||
if (!polledCommandDelivered) {
|
||||
polledCommandDelivered = true;
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return {
|
||||
data: {
|
||||
id: 99,
|
||||
commandType: "DISCOVER_SHELLY",
|
||||
payload: {
|
||||
candidateIps: ["10.1.0.31"],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return { data: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (String(url).endsWith("/commands/99/result")) {
|
||||
resultPosts.push({
|
||||
url,
|
||||
body,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return { data: { acknowledged: true } };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (String(url) === "http://10.1.0.31/shelly") {
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return {
|
||||
mac: "AA:BB:CC:DD:EE:FF",
|
||||
model: "Shelly Plus 1PM",
|
||||
num_switches: 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
const sockets = [];
|
||||
@@ -160,19 +287,28 @@ test("startAgent retries the broker tunnel and reports degraded heartbeats while
|
||||
});
|
||||
|
||||
assert.equal(heartbeats[0].body.status, "DEGRADED");
|
||||
assert.equal(heartbeats[0].body.discovery_status, "BROKER_DISCONNECTED");
|
||||
assert.equal(heartbeats[0].body.metadata.broker_connected, false);
|
||||
assert.equal("discovery_status" in heartbeats[0].body, false);
|
||||
assert.equal(sockets.length, 1);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
assert.equal(resultPosts.length, 1);
|
||||
assert.equal(resultPosts[0].body.ok, true);
|
||||
assert.equal(Array.isArray(resultPosts[0].body.payload.inventory), true);
|
||||
assert.equal(resultPosts[0].body.payload.inventory[0].device_id, "AA:BB:CC:DD:EE:FF");
|
||||
|
||||
sockets[0].readyState = 1;
|
||||
sockets[0].emit("open");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
assert.ok(heartbeats.some((heartbeat) => heartbeat.body.discovery_status === "READY"));
|
||||
assert.ok(heartbeats.some((heartbeat) => heartbeat.body.metadata.broker_connected === true));
|
||||
assert.ok(heartbeats.every((heartbeat) => !("discovery_status" in heartbeat.body)));
|
||||
|
||||
sockets[0].emit("close");
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
|
||||
assert.ok(heartbeats.some((heartbeat) => heartbeat.body.discovery_status === "BROKER_DISCONNECTED"));
|
||||
assert.ok(heartbeats.some((heartbeat) => heartbeat.body.status === "DEGRADED" && heartbeat.body.metadata.broker_connected === false));
|
||||
assert.equal(sockets.length, 2);
|
||||
|
||||
agent.stop();
|
||||
|
||||
Reference in New Issue
Block a user