fix(cli): read node liveness from the recorded node.list fact and drop dead nodes-CLI guards (#124348)

Four nodes-CLI repairs at their owners:

- nodes status --last-connected joined a second pairing-scoped RPC
  (node.pair.list) to reconstruct last-connection times the gateway already
  records on every node.list row (lastConnectedAtMs, max of stored pairing
  history and live connection, since 2dcd47d4f4). The client-side join also
  preferred stored history over a live connection instead of taking the max,
  and made the command fail for callers whose auth grants operator.read but
  not operator.pairing. The filter now reads the recorded fact;
  connectedAtMs covers gateways predating it.
- nodes list --connected blanked pending rows and then printed "Pending: 0"
  while requests waited — asserting a fact it never checked. Pending rows
  carry no connection state to filter on; they now always show.
- nodes remove/rename carried unreachable !nodeId guards (resolveCliNodeId
  throws "node required" on blank input and every parse layer drops id-less
  rows) whose dead text pointed at the wrong command (nodes pending targets
  pairing requests, not paired nodes). Guards deleted; the reachable empty
  --name branch now hints at nodes list.
- status.node-mode derived node-service liveness from the formatted
  runtimeShort display string — parsing a fact back out of its own
  projection, dead in all first-party call paths (both callers pass the
  runtime object it is derived from). Field and branch deleted.

e2e test updated to pin the single-RPC shape (asserts node.pair.list is NOT
called); node-mode fixtures now use the production shape.
This commit is contained in:
Peter Steinberger
2026-08-15 19:19:13 -07:00
committed by GitHub
parent df2b83f7ae
commit ca849506f3
5 changed files with 26 additions and 46 deletions
+13 -13
View File
@@ -358,25 +358,23 @@ describe("cli program (nodes basics)", () => {
expect(output).toContain("Catalog Only");
});
it("runs nodes status --last-connected and filters by age", async () => {
it("runs nodes status --last-connected using the recorded node.list fact", async () => {
const now = Date.now();
const methods: string[] = [];
programGatewayCallMock.mockImplementation(async (...args: unknown[]) => {
const opts = (args[0] ?? {}) as { method?: string };
methods.push(opts.method ?? "");
if (opts.method === "node.list") {
return {
ts: now,
nodes: [
{ nodeId: "n1", displayName: "One", connected: false },
{ nodeId: "n2", displayName: "Two", connected: false },
],
};
}
if (opts.method === "node.pair.list") {
return {
pending: [],
paired: [
{ nodeId: "n1", lastConnectedAtMs: now - 1_000 },
{ nodeId: "n2", lastConnectedAtMs: now - 2 * 24 * 60 * 60 * 1000 },
{ nodeId: "n1", displayName: "One", connected: false, lastConnectedAtMs: now - 1_000 },
{
nodeId: "n2",
displayName: "Two",
connected: false,
lastConnectedAtMs: now - 2 * 24 * 60 * 60 * 1000,
},
],
};
}
@@ -384,7 +382,9 @@ describe("cli program (nodes basics)", () => {
});
await runProgram(["nodes", "status", "--last-connected", "24h"]);
expectGatewayRequest("node.pair.list", {});
// The gateway records lastConnectedAtMs on node.list rows; re-joining
// node.pair.list broke --last-connected for read-scoped callers.
expect(methods).not.toContain("node.pair.list");
const output = getRuntimeOutput();
expect(output).toContain("One");
expect(output).not.toContain("Two");