Files
openclaw/src/gateway/node-agent-cli-runtime.test.ts
Peter Steinberger f167089d61 feat(nodes): continue Claude catalog sessions on paired nodes via streaming CLI agent runs (#105833)
* feat(gateway): stream node.invoke progress with idle timeouts and cancel

* feat(node-host): opt-in approval-gated claude cli agent run command

* feat(agents): run node-placed claude-cli turns through streaming node invoke

* feat(anthropic): continue claude catalog sessions on advertising nodes

* docs(nodes): document paired-node claude agent runs and continuation

* chore(plugin-sdk): admit spawn-invocation helper into surface budget

* fix(nodes): harden streaming invoke and node run policy from review findings

* test(node-host): prove multi-token tool-policy argv fails closed

* fix(nodes): allowlist cancel event for apps and break node-host type cycle

* chore(protocol): regenerate Swift gateway models for node invoke progress

* chore(node-host): drop test-only export surface flagged by deadcode ratchet

* style(node-host): merge invoke-types imports

* refactor(nodes): split node agent run modules to satisfy the LOC ratchet

* chore(protocol): regenerate android gateway models and sync export baseline

* chore(plugin-sdk): admit spawn-invocation helper and inherited deprecated drift into surface budget

* chore(plugin-sdk): tolerate inherited agent-core deprecated export

* fix(anthropic): drop node param parser orphaned by upstream catalog split

* ci: sync unused-export baseline with healed main

* test(agents): adopt renamed cli backend prepare helper

* style(node-host): format claude run spawn invocation

* ci: retrigger checks

* chore(plugin-sdk): pin surface budgets to healed-base counts

* fix(protocol): restore untyped lazy validators after upstream style change
2026-07-13 07:39:22 -07:00

80 lines
2.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
get: vi.fn(),
invoke: vi.fn(),
getRuntimeConfig: vi.fn(() => ({})),
isNodeCommandAllowed: vi.fn(),
resolveNodeCommandAllowlist: vi.fn(() => new Set<string>()),
}));
vi.mock("./server-plugin-fallback-context.js", () => ({
getFallbackGatewayContext: () => ({
getRuntimeConfig: mocks.getRuntimeConfig,
nodeRegistry: { get: mocks.get, invoke: mocks.invoke },
}),
}));
vi.mock("./node-command-policy.js", () => ({
isNodeCommandAllowed: mocks.isNodeCommandAllowed,
resolveNodeCommandAllowlist: mocks.resolveNodeCommandAllowlist,
}));
import { invokeNodeClaudeCliRun } from "./node-agent-cli-runtime.js";
describe("invokeNodeClaudeCliRun", () => {
beforeEach(() => {
mocks.get.mockReset();
mocks.invoke.mockReset();
mocks.getRuntimeConfig.mockClear();
mocks.resolveNodeCommandAllowlist.mockClear();
mocks.isNodeCommandAllowed.mockReset();
mocks.get.mockReturnValue({
connId: "conn-1",
nodeId: "node-1",
commands: ["agent.cli.claude.run.v1"],
});
});
it("fails closed when Gateway node command policy denies the agent run", async () => {
mocks.isNodeCommandAllowed.mockReturnValue({ ok: false, reason: "denyCommands" });
await expect(
invokeNodeClaudeCliRun({
nodeId: "node-1",
argv: ["-p"],
stdin: "hello",
timeoutMs: 10_000,
idleTimeoutMs: 1_000,
onProgress: () => {},
}),
).resolves.toEqual({
ok: false,
error: {
code: "PERMISSION_DENIED",
message:
"paired-node Claude CLI agent runs are blocked by node command policy (denyCommands)",
},
});
expect(mocks.invoke).not.toHaveBeenCalled();
});
it("dispatches only after the command policy allows the advertised command", async () => {
mocks.isNodeCommandAllowed.mockReturnValue({ ok: true });
mocks.invoke.mockResolvedValue({ ok: true });
await expect(
invokeNodeClaudeCliRun({
nodeId: "node-1",
argv: ["-p"],
stdin: "hello",
timeoutMs: 10_000,
idleTimeoutMs: 1_000,
onProgress: () => {},
}),
).resolves.toEqual({ ok: true });
expect(mocks.resolveNodeCommandAllowlist).toHaveBeenCalledOnce();
expect(mocks.invoke).toHaveBeenCalledOnce();
});
});