mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:31:33 +00:00
* refactor: remove gateway and channel dead exports * style: format config reload test * refactor: remove newly dead gateway exports * test: preserve approval audience coverage * refactor: preserve gateway contracts while pruning exports * test: retain gateway regression coverage * test: restore gateway policy coverage * test: close dead-export CI gaps * fix: reconcile dead exports with latest main * refactor: remove newly dead gateway runner export * test: remove private worker verifier coverage * test: preserve weak secret docs coverage * fix: avoid gateway health import cycle * fix: preserve node pairing type contract * style: format talk relay test * fix: preserve gateway protocol generation contract * chore: refresh dead export baseline * fix: preserve loaded target compatibility exports * fix: preserve plugin SDK declaration resolution * chore: refresh dead export baseline * chore: refresh dead export baseline * test(gateway): derive private worker service options
538 lines
17 KiB
TypeScript
538 lines
17 KiB
TypeScript
/**
|
|
* Gateway node catalog regression tests.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { createKnownNodeCatalog, getKnownNode, listKnownNodes } from "./node-catalog.js";
|
|
|
|
type CatalogInput = Parameters<typeof createKnownNodeCatalog>[0];
|
|
type TestPairedDevice = CatalogInput["pairedDevices"][number];
|
|
type TestPairedNode = NonNullable<CatalogInput["pairedNodes"]>[number];
|
|
type TestPendingNode = NonNullable<CatalogInput["pendingNodes"]>[number];
|
|
|
|
function pairedDevice(overrides: Partial<TestPairedDevice> = {}): TestPairedDevice {
|
|
return {
|
|
deviceId: "mac-1",
|
|
publicKey: "public-key",
|
|
displayName: "Mac",
|
|
clientId: "openclaw-macos",
|
|
clientMode: "node",
|
|
role: "node",
|
|
roles: ["node"],
|
|
tokens: {
|
|
node: {
|
|
token: "current-token",
|
|
role: "node",
|
|
scopes: [],
|
|
createdAtMs: 1,
|
|
},
|
|
},
|
|
createdAtMs: 1,
|
|
approvedAtMs: 99,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function pairedNode(overrides: Partial<TestPairedNode> = {}): TestPairedNode {
|
|
return {
|
|
nodeId: "mac-1",
|
|
platform: "macos",
|
|
caps: ["camera"],
|
|
commands: ["system.run"],
|
|
createdAtMs: 1,
|
|
approvedAtMs: 100,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function pendingNode(overrides: Partial<TestPendingNode> = {}): TestPendingNode {
|
|
return {
|
|
requestId: "request-1",
|
|
nodeId: "mac-1",
|
|
platform: "macos",
|
|
caps: ["camera", "screen"],
|
|
commands: ["screen.snapshot", "system.run"],
|
|
permissions: { camera: true, screen: true },
|
|
ts: 200,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("gateway/node-catalog", () => {
|
|
it("filters paired nodes by active node token instead of sticky historical roles", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [
|
|
pairedDevice({
|
|
deviceId: "legacy-mac",
|
|
displayName: "Peter's Mac Studio",
|
|
clientId: "clawdbot-macos",
|
|
tokens: {
|
|
node: {
|
|
token: "legacy-token",
|
|
role: "node",
|
|
scopes: [],
|
|
createdAtMs: 1,
|
|
revokedAtMs: 2,
|
|
},
|
|
},
|
|
approvedAtMs: 1,
|
|
}),
|
|
pairedDevice({
|
|
deviceId: "current-mac",
|
|
displayName: "Peter's Mac Studio",
|
|
approvedAtMs: 1,
|
|
}),
|
|
],
|
|
pairedNodes: [],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
expect(listKnownNodes(catalog).map((node) => node.nodeId)).toEqual(["current-mac"]);
|
|
});
|
|
|
|
it("builds one public node view from paired and live state", () => {
|
|
const connectedAtMs = 123;
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice({ remoteIp: "100.0.0.10" })],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
displayName: "Mac",
|
|
version: "1.2.0",
|
|
coreVersion: "1.2.0",
|
|
uiVersion: "1.2.0",
|
|
remoteIp: "100.0.0.9",
|
|
approvedAtMs: 100,
|
|
}),
|
|
],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "mac-1",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
clientId: "openclaw-macos",
|
|
clientMode: "node",
|
|
displayName: "Mac",
|
|
platform: "macos",
|
|
version: "1.2.3",
|
|
declaredCaps: ["camera", "screen"],
|
|
caps: ["camera", "screen"],
|
|
declaredCommands: ["screen.snapshot", "system.run"],
|
|
commands: ["screen.snapshot", "system.run"],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
remoteIp: "100.0.0.11",
|
|
pathEnv: "/usr/bin:/bin",
|
|
connectedAtMs,
|
|
lastActiveAtMs: 120,
|
|
presenceUpdatedAtMs: 125,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(getKnownNode(catalog, "mac-1")).toMatchObject({
|
|
nodeId: "mac-1",
|
|
displayName: "Mac",
|
|
clientId: "openclaw-macos",
|
|
clientMode: "node",
|
|
remoteIp: "100.0.0.11",
|
|
caps: ["camera", "screen"],
|
|
commands: ["screen.snapshot", "system.run"],
|
|
pathEnv: "/usr/bin:/bin",
|
|
approvedAtMs: 100,
|
|
connectedAtMs,
|
|
lastActiveAtMs: 120,
|
|
presenceUpdatedAtMs: 125,
|
|
lastSeenAtMs: connectedAtMs,
|
|
lastSeenReason: "connect",
|
|
paired: true,
|
|
connected: true,
|
|
});
|
|
});
|
|
|
|
it("surfaces paired-node metadata while the node is offline", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice()],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
caps: ["system"],
|
|
lastSeenAtMs: 456,
|
|
lastSeenReason: "silent_push",
|
|
approvedAtMs: 123,
|
|
}),
|
|
],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
expect(getKnownNode(catalog, "mac-1")).toMatchObject({
|
|
nodeId: "mac-1",
|
|
caps: ["system"],
|
|
commands: ["system.run"],
|
|
approvedAtMs: 123,
|
|
lastSeenAtMs: 456,
|
|
lastSeenReason: "silent_push",
|
|
paired: true,
|
|
connected: false,
|
|
});
|
|
});
|
|
|
|
it("uses the newest durable last-seen source for offline nodes", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [
|
|
pairedDevice({
|
|
deviceId: "ios-1",
|
|
displayName: "iPhone",
|
|
lastSeenAtMs: 300,
|
|
lastSeenReason: "silent_push",
|
|
approvedAtMs: 10,
|
|
}),
|
|
],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
nodeId: "ios-1",
|
|
platform: "ios",
|
|
caps: [],
|
|
commands: [],
|
|
lastConnectedAtMs: 200,
|
|
lastSeenAtMs: 100,
|
|
lastSeenReason: "bg_app_refresh",
|
|
approvedAtMs: 11,
|
|
}),
|
|
],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "ios-1");
|
|
expect(node?.lastSeenAtMs).toBe(300);
|
|
expect(node?.lastSeenReason).toBe("silent_push");
|
|
});
|
|
|
|
it("prefers the live command surface for connected nodes", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
caps: ["system"],
|
|
approvedAtMs: 123,
|
|
}),
|
|
],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "mac-1",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: "Mac",
|
|
platform: "macos",
|
|
declaredCaps: ["canvas"],
|
|
caps: ["canvas"],
|
|
declaredCommands: ["canvas.snapshot"],
|
|
commands: ["canvas.snapshot"],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
connectedAtMs: 1,
|
|
},
|
|
],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "mac-1");
|
|
expect(node?.caps).toEqual(["canvas"]);
|
|
expect(node?.commands).toEqual(["canvas.snapshot"]);
|
|
expect(node?.connected).toBe(true);
|
|
});
|
|
|
|
it("reports pending first approval without making declarations effective", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice({ deviceId: "new-node" })],
|
|
pairedNodes: [],
|
|
pendingNodes: [pendingNode({ nodeId: "new-node", displayName: "Pending Mac" })],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "new-node");
|
|
expect(node?.displayName).toBe("Mac");
|
|
expect(node?.approvalState).toBe("pending-approval");
|
|
expect(node?.pendingRequestId).toBe("request-1");
|
|
expect(node?.pendingDeclaredCaps).toEqual(["camera", "screen"]);
|
|
expect(node?.pendingDeclaredCommands).toEqual(["screen.snapshot", "system.run"]);
|
|
expect(node?.pendingDeclaredPermissions).toEqual({ camera: true, screen: true });
|
|
expect(node?.caps).toEqual([]);
|
|
expect(node?.commands).toEqual([]);
|
|
expect(node?.permissions).toBeUndefined();
|
|
});
|
|
|
|
it("uses pending request metadata as the final fallback for pending-only nodes", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [],
|
|
pairedNodes: [],
|
|
pendingNodes: [
|
|
pendingNode({
|
|
nodeId: "new-node",
|
|
clientId: "openclaw-linux",
|
|
clientMode: "node",
|
|
displayName: "Pending Node",
|
|
platform: "linux",
|
|
version: "1.2.3",
|
|
coreVersion: "1.2.4",
|
|
uiVersion: "1.2.5",
|
|
deviceFamily: "desktop",
|
|
modelIdentifier: "x86_64",
|
|
remoteIp: "100.0.0.20",
|
|
}),
|
|
],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
expect(getKnownNode(catalog, "new-node")).toMatchObject({
|
|
nodeId: "new-node",
|
|
clientId: "openclaw-linux",
|
|
clientMode: "node",
|
|
displayName: "Pending Node",
|
|
platform: "linux",
|
|
version: "1.2.3",
|
|
coreVersion: "1.2.4",
|
|
uiVersion: "1.2.5",
|
|
deviceFamily: "desktop",
|
|
modelIdentifier: "x86_64",
|
|
remoteIp: "100.0.0.20",
|
|
approvalState: "pending-approval",
|
|
pendingRequestId: "request-1",
|
|
caps: [],
|
|
commands: [],
|
|
paired: false,
|
|
connected: false,
|
|
});
|
|
expect(getKnownNode(catalog, "new-node")?.permissions).toBeUndefined();
|
|
});
|
|
|
|
it("preserves pending first approval when a metadata reconnect omits permissions", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice({ deviceId: "new-node" })],
|
|
pairedNodes: [],
|
|
pendingNodes: [pendingNode({ nodeId: "new-node" })],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "new-node",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: "New Node",
|
|
platform: "macos",
|
|
declaredCaps: ["camera", "screen"],
|
|
caps: [],
|
|
declaredCommands: ["screen.snapshot", "system.run"],
|
|
commands: [],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
connectedAtMs: 1,
|
|
},
|
|
],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "new-node");
|
|
expect(node?.approvalState).toBe("pending-approval");
|
|
expect(node?.pendingRequestId).toBe("request-1");
|
|
expect(node?.pendingDeclaredPermissions).toEqual({ camera: true, screen: true });
|
|
expect(node?.permissions).toBeUndefined();
|
|
});
|
|
|
|
it("reports pending reapproval without making declarations effective", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice()],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
caps: ["camera"],
|
|
commands: ["screen.snapshot"],
|
|
permissions: { camera: true },
|
|
}),
|
|
],
|
|
pendingNodes: [pendingNode()],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "mac-1",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: "Mac",
|
|
platform: "macos",
|
|
declaredCaps: ["camera", "screen"],
|
|
caps: ["camera"],
|
|
declaredCommands: ["screen.snapshot", "system.run"],
|
|
commands: ["screen.snapshot"],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
declaredPermissions: { camera: true, screen: true },
|
|
permissions: { camera: true },
|
|
connectedAtMs: 1,
|
|
},
|
|
],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "mac-1");
|
|
expect(node?.approvalState).toBe("pending-reapproval");
|
|
expect(node?.pendingRequestId).toBe("request-1");
|
|
expect(node?.pendingDeclaredCaps).toEqual(["camera", "screen"]);
|
|
expect(node?.pendingDeclaredCommands).toEqual(["screen.snapshot", "system.run"]);
|
|
expect(node?.pendingDeclaredPermissions).toEqual({ camera: true, screen: true });
|
|
expect(node?.caps).toEqual(["camera"]);
|
|
expect(node?.commands).toEqual(["screen.snapshot"]);
|
|
expect(node?.permissions).toEqual({ camera: true });
|
|
});
|
|
|
|
it("ignores a pending reapproval that no longer matches the live declaration", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice()],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
caps: ["camera"],
|
|
commands: ["screen.snapshot"],
|
|
permissions: { camera: true },
|
|
}),
|
|
],
|
|
pendingNodes: [pendingNode()],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "mac-1",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: "Mac",
|
|
platform: "macos",
|
|
declaredCaps: ["camera"],
|
|
caps: ["camera"],
|
|
declaredCommands: ["screen.snapshot"],
|
|
commands: ["screen.snapshot"],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
declaredPermissions: { camera: true },
|
|
permissions: { camera: true },
|
|
connectedAtMs: 1,
|
|
},
|
|
],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "mac-1");
|
|
expect(node?.approvalState).toBe("approved");
|
|
expect(node?.pendingRequestId).toBeUndefined();
|
|
expect(node?.pendingDeclaredCaps).toBeUndefined();
|
|
expect(node?.pendingDeclaredCommands).toBeUndefined();
|
|
expect(node?.pendingDeclaredPermissions).toBeUndefined();
|
|
expect(node?.caps).toEqual(["camera"]);
|
|
expect(node?.commands).toEqual(["screen.snapshot"]);
|
|
expect(node?.permissions).toEqual({ camera: true });
|
|
});
|
|
|
|
it("ignores malformed node capability entries instead of throwing", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [],
|
|
pairedNodes: [],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "bad-node",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: "Bad Node",
|
|
caps: ["camera", undefined],
|
|
commands: ["system.run", null],
|
|
connectedAtMs: 1,
|
|
} as never,
|
|
],
|
|
});
|
|
|
|
const nodes = listKnownNodes(catalog);
|
|
expect(nodes).toHaveLength(1);
|
|
expect(nodes[0]?.nodeId).toBe("bad-node");
|
|
expect(nodes[0]?.caps).toEqual(["camera"]);
|
|
expect(nodes[0]?.commands).toEqual(["system.run"]);
|
|
});
|
|
|
|
it("normalizes non-string scalar fields from malformed pairing records (no nodes-status crash)", () => {
|
|
// Paired/pending records are blind-cast from disk, so every formatter-facing scalar can be a
|
|
// non-string; before this guard a `nodes status` formatter (.trim() / sanitizeTerminalText) threw.
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [
|
|
pairedDevice({
|
|
deviceId: "mac-1",
|
|
displayName: 654 as unknown as string,
|
|
clientId: 456 as unknown as string,
|
|
clientMode: 789 as unknown as string,
|
|
}),
|
|
],
|
|
pairedNodes: [
|
|
pairedNode({
|
|
nodeId: "mac-1",
|
|
version: 123 as unknown as string,
|
|
displayName: 321 as unknown as string,
|
|
platform: 11 as unknown as string,
|
|
remoteIp: 22 as unknown as string,
|
|
deviceFamily: 33 as unknown as string,
|
|
modelIdentifier: 44 as unknown as string,
|
|
}),
|
|
],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "mac-1");
|
|
expect(node?.version).toBeUndefined();
|
|
expect(node?.displayName).toBeUndefined();
|
|
expect(node?.clientId).toBeUndefined();
|
|
expect(node?.clientMode).toBeUndefined();
|
|
expect(node?.platform).toBeUndefined();
|
|
expect(node?.remoteIp).toBeUndefined();
|
|
expect(node?.deviceFamily).toBeUndefined();
|
|
expect(node?.modelIdentifier).toBeUndefined();
|
|
});
|
|
|
|
it("falls through a non-string higher-priority scalar to a valid lower-priority value", () => {
|
|
// A corrupted live (higher-priority) scalar must be treated as ABSENT so the valid paired
|
|
// (lower-priority) value still surfaces, instead of the malformed value suppressing it.
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [pairedDevice({ deviceId: "mac-1" })],
|
|
pairedNodes: [
|
|
pairedNode({ nodeId: "mac-1", displayName: "Node Display", platform: "linux" }),
|
|
],
|
|
connectedNodes: [
|
|
{
|
|
nodeId: "mac-1",
|
|
connId: "conn-1",
|
|
client: {} as never,
|
|
displayName: 42 as unknown as string,
|
|
platform: {} as unknown as string,
|
|
declaredCaps: [],
|
|
caps: [],
|
|
declaredCommands: [],
|
|
commands: [],
|
|
declaredNodePluginTools: [],
|
|
nodePluginTools: [],
|
|
nodeSkills: [],
|
|
connectedAtMs: 1,
|
|
},
|
|
],
|
|
});
|
|
|
|
const node = getKnownNode(catalog, "mac-1");
|
|
expect(node?.displayName).toBe("Node Display");
|
|
expect(node?.platform).toBe("linux");
|
|
});
|
|
|
|
it("drops blind-cast pairing entries whose required id is not a string", () => {
|
|
const catalog = createKnownNodeCatalog({
|
|
pairedDevices: [
|
|
pairedDevice({ deviceId: "good-node" }),
|
|
// A corrupted pairing file can carry a non-string id; without the catalog id guard
|
|
// these would key the maps and crash listKnownNodes' nodeId.localeCompare sort.
|
|
pairedDevice({ deviceId: 7 as unknown as string }),
|
|
],
|
|
pairedNodes: [
|
|
pairedNode({ nodeId: "good-node" }),
|
|
pairedNode({ nodeId: {} as unknown as string }),
|
|
],
|
|
pendingNodes: [pendingNode({ nodeId: [] as unknown as string })],
|
|
connectedNodes: [],
|
|
});
|
|
|
|
const nodes = listKnownNodes(catalog);
|
|
expect(nodes.map((entry) => entry.nodeId)).toEqual(["good-node"]);
|
|
expect(getKnownNode(catalog, "good-node")).not.toBeNull();
|
|
});
|
|
});
|