mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
d8d2f83cc1
Catalog session rows (sidebar context menu + click), the built-in viewer header, and a new "Open Codex/Claude sessions in" preference can launch the native CLI (codex resume / claude --resume) in the operator terminal on the machine that owns the session. - Gateway-local sessions spawn through the existing terminal launch policy (sandbox/enabled gates preserved) with the resume command in the session cwd. - Paired-node sessions run through a new seq-ordered node PTY relay: a duplex node-host command streams PTY output via node.invoke.progress and receives keystrokes/resize via a new node.invoke.input event, behind the unchanged terminal.* client protocol (TerminalSessionManager gains a backend abstraction; node relay reuses the streaming-invoke controller). - Owner boundary: each plugin owns its resume command and builds argv from a validated thread id; the gateway routes node opens through the node command allowlist and plugin invoke policy (no advertisement-only trust), and nodes re-verify session eligibility before spawning. - UI setting catalogOpenTarget + canOpenTerminal capability gate every entry point; capability requires the owning host to actually have the CLI. Node PATH is normalized before command-availability probes, Windows .cmd/.bat shims spawn via ComSpec, and catalog terminal opens reattach persisted tabs before opening the new tab.
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { dispatchNodeInvokeInput, registerNodeInvokeInputHandler } from "./runtime.js";
|
|
|
|
function createTarget() {
|
|
return {
|
|
nextInputSeq: 0,
|
|
pendingInput: [] as Array<{ payloadJSON: string; bytes: number }>,
|
|
pendingInputBytes: 0,
|
|
inputFailed: false,
|
|
abort: vi.fn(),
|
|
};
|
|
}
|
|
|
|
describe("node-host invoke input dispatch", () => {
|
|
it("buffers frames before registration and flushes them in order", () => {
|
|
const input = vi.fn();
|
|
const target = createTarget();
|
|
|
|
expect(dispatchNodeInvokeInput(target, 0, "first")).toBe(true);
|
|
expect(dispatchNodeInvokeInput(target, 1, "second")).toBe(true);
|
|
expect(input).not.toHaveBeenCalled();
|
|
|
|
registerNodeInvokeInputHandler(target, input);
|
|
expect(input.mock.calls).toEqual([["first"], ["second"]]);
|
|
});
|
|
|
|
it("drops duplicate sequence numbers", () => {
|
|
const input = vi.fn();
|
|
const target = createTarget();
|
|
registerNodeInvokeInputHandler(target, input);
|
|
|
|
expect(dispatchNodeInvokeInput(undefined, 0, "unknown")).toBe(false);
|
|
expect(dispatchNodeInvokeInput(target, 0, "first")).toBe(true);
|
|
expect(dispatchNodeInvokeInput(target, 0, "duplicate")).toBe(false);
|
|
expect(dispatchNodeInvokeInput(target, 1, "second")).toBe(true);
|
|
expect(input.mock.calls).toEqual([["first"], ["second"]]);
|
|
});
|
|
|
|
it("aborts without delivering partial input when the pre-spawn buffer overflows", () => {
|
|
const input = vi.fn();
|
|
const target = createTarget();
|
|
const chunk = "x".repeat(16 * 1024 - 1);
|
|
|
|
for (let seq = 0; seq < 4; seq += 1) {
|
|
expect(dispatchNodeInvokeInput(target, seq, `${seq}${chunk}`)).toBe(true);
|
|
}
|
|
expect(dispatchNodeInvokeInput(target, 4, `4${chunk}`)).toBe(false);
|
|
registerNodeInvokeInputHandler(target, input);
|
|
expect(input).not.toHaveBeenCalled();
|
|
expect(target.pendingInput).toEqual([]);
|
|
expect(target.abort).toHaveBeenCalledOnce();
|
|
expect(dispatchNodeInvokeInput(target, 5, "continued")).toBe(false);
|
|
});
|
|
|
|
it("tolerates sequence gaps", () => {
|
|
const input = vi.fn();
|
|
const target = createTarget();
|
|
registerNodeInvokeInputHandler(target, input);
|
|
|
|
expect(dispatchNodeInvokeInput(target, 2, "gap")).toBe(true);
|
|
expect(dispatchNodeInvokeInput(target, 3, "next")).toBe(true);
|
|
expect(input.mock.calls).toEqual([["gap"], ["next"]]);
|
|
});
|
|
});
|