mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:20:44 +00:00
fix(cli): keep nodes list aligned with nodes status (#72619)
* fix(cli): keep nodes list aligned with nodes status * fix(clownfish): address review for ghcrawl-156588-autonomous-smoke (1) * fix(cli): keep nodes list aligned with nodes status
This commit is contained in:
@@ -62,6 +62,183 @@ describe("cli program (nodes basics)", () => {
|
||||
program = createProgram();
|
||||
});
|
||||
|
||||
it("runs nodes list with the effective paired node view while preserving paired metadata", async () => {
|
||||
const now = Date.now();
|
||||
callGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
const opts = (args[0] ?? {}) as { method?: string };
|
||||
if (opts.method === "node.pair.list") {
|
||||
return {
|
||||
pending: [{ requestId: "r1", nodeId: "pending-node", ts: now - 10_000 }],
|
||||
paired: [
|
||||
{
|
||||
nodeId: "paired-store",
|
||||
displayName: "Stale paired name",
|
||||
remoteIp: "10.0.0.1",
|
||||
token: "paired-token",
|
||||
lastConnectedAtMs: now - 5_000,
|
||||
},
|
||||
{
|
||||
nodeId: "pair-only",
|
||||
displayName: "Pair Only",
|
||||
token: "pair-only-token",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (opts.method === "node.list") {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
nodeId: "paired-store",
|
||||
displayName: "Effective paired name",
|
||||
remoteIp: "10.0.0.2",
|
||||
connected: true,
|
||||
connectedAtMs: now - 1_000,
|
||||
},
|
||||
{
|
||||
nodeId: "catalog-only",
|
||||
displayName: "Catalog Only",
|
||||
remoteIp: "10.0.0.3",
|
||||
paired: true,
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
nodeId: "effective-only-unknown",
|
||||
displayName: "Effective Only Unknown",
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
nodeId: "unpaired-live",
|
||||
displayName: "Unpaired Live",
|
||||
paired: false,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await runProgram(["nodes", "list", "--json"]);
|
||||
|
||||
expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "node.pair.list" }));
|
||||
expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "node.list" }));
|
||||
expect(runtime.writeJson).toHaveBeenCalledWith({
|
||||
pending: [{ requestId: "r1", nodeId: "pending-node", ts: now - 10_000 }],
|
||||
paired: [
|
||||
expect.objectContaining({
|
||||
nodeId: "paired-store",
|
||||
displayName: "Effective paired name",
|
||||
remoteIp: "10.0.0.2",
|
||||
lastConnectedAtMs: now - 5_000,
|
||||
connected: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
nodeId: "catalog-only",
|
||||
displayName: "Catalog Only",
|
||||
paired: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
nodeId: "pair-only",
|
||||
displayName: "Pair Only",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(JSON.stringify(runtime.writeJson.mock.calls[0]?.[0])).not.toContain("paired-token");
|
||||
expect(JSON.stringify(runtime.writeJson.mock.calls[0]?.[0])).not.toContain("pair-only-token");
|
||||
const output = getRuntimeOutput();
|
||||
expect(output).toContain("Pending: 1 · Paired: 3");
|
||||
expect(output).not.toContain("Effective Only Unknown");
|
||||
expect(output).not.toContain("unpaired-live");
|
||||
});
|
||||
|
||||
it("runs unfiltered nodes list with pairing data when node.list is unavailable", async () => {
|
||||
callGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
const opts = (args[0] ?? {}) as { method?: string };
|
||||
if (opts.method === "node.pair.list") {
|
||||
return {
|
||||
pending: [],
|
||||
paired: [
|
||||
{
|
||||
nodeId: "pairing-scoped",
|
||||
displayName: "Pairing Scoped",
|
||||
remoteIp: "10.0.0.9",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (opts.method === "node.list") {
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await runProgram(["nodes", "list"]);
|
||||
|
||||
const output = getRuntimeOutput();
|
||||
expect(output).toContain("Pending: 0 · Paired: 1");
|
||||
expect(output).toContain("Pairing Scoped");
|
||||
});
|
||||
|
||||
it("sanitizes untrusted nodes list table fields while preserving JSON values", async () => {
|
||||
const now = Date.now();
|
||||
callGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
const opts = (args[0] ?? {}) as { method?: string };
|
||||
if (opts.method === "node.pair.list") {
|
||||
return {
|
||||
pending: [
|
||||
{
|
||||
requestId: "request\u001b[2K-1",
|
||||
nodeId: "pending-node",
|
||||
displayName: "Pending\u001b[1A\nNode",
|
||||
remoteIp: "10.0.0.4\rrewritten",
|
||||
ts: now - 1_000,
|
||||
},
|
||||
],
|
||||
paired: [
|
||||
{
|
||||
nodeId: "paired-node",
|
||||
displayName: "Paired\u001b[2K\nNode",
|
||||
remoteIp: "10.0.0.5\rrewritten",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (opts.method === "node.list") {
|
||||
throw new Error("older gateway");
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await runProgram(["nodes", "list"]);
|
||||
|
||||
const output = getRuntimeOutput();
|
||||
expect(output).not.toContain("\u001b");
|
||||
expect(output).not.toContain("[2K");
|
||||
expect(output).toContain("Pending\\nNode");
|
||||
expect(output).toContain("Paired\\nNode");
|
||||
expect(output).toContain("10.0.0.5\\rrewritten");
|
||||
|
||||
runtime.log.mockClear();
|
||||
await runProgram(["nodes", "list", "--json"]);
|
||||
|
||||
expect(runtime.writeJson).toHaveBeenCalledWith({
|
||||
pending: [
|
||||
expect.objectContaining({
|
||||
requestId: "request\u001b[2K-1",
|
||||
displayName: "Pending\u001b[1A\nNode",
|
||||
}),
|
||||
],
|
||||
paired: [
|
||||
expect.objectContaining({
|
||||
nodeId: "paired-node",
|
||||
displayName: "Paired\u001b[2K\nNode",
|
||||
remoteIp: "10.0.0.5\rrewritten",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("runs nodes list --connected and filters to connected nodes", async () => {
|
||||
const now = Date.now();
|
||||
callGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
|
||||
Reference in New Issue
Block a user