Files
paseo/packages/cli/src/cli-surface.test.ts
T
Derick WellsandMohamed Boudra f22775a8e1 Default agent-to-agent messages to steering in CLI and MCP (#5164)
* Let agents steer a running turn from send_agent_prompt and paseo send

When an agent used the send_agent_prompt MCP tool on a target that was
mid-turn, sendPromptToAgent ran without activeTurnBehavior, so the
prompt path fell through to replaceAgentRun. That cancelled the target's
active turn and every provider subagent it had in flight. The web UI,
child finish notifications and schedules already pass "steer"; the MCP
tool and the CLI had no way to ask for it.

Add an optional activeTurnBehavior input to send_agent_prompt. It
defaults to "steer" for agent-scoped callers and to "interrupt" for
top-level callers, so existing top-level behaviour is unchanged. Add
--steer to paseo send. Document both and cover them with tests.

* fix(agents): retain finish notifications during steering acknowledgement

Observe completion before dispatch and hold delivery until acceptance. A replacement discards the displaced turn observation and starts a fresh listener before its iterator runs.

* fix(agents): preserve notification ownership when follow-ups fail

Keep the previous observation until dispatch succeeds and suppress duplicate delivery during acknowledgement. Decide whether to retain the running turn from the dispatch result, including side commands requested with interrupt.

* fix(agents): deduplicate notifications across overlapping follow-ups

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-10-17 20:39:05 +07:00

142 lines
6.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createCli } from "./cli.js";
describe("canonical CLI surface", () => {
it.each(["send", "agent send"])("%s accepts an explicit behavior override", async (path) => {
const cli = createCli();
const owner =
path === "send" ? cli : cli.commands.find((command) => command.name() === "agent")!;
const send = owner.commands.find((command) => command.name() === "send")!;
send.action(() => {});
await send.parseAsync(["target", "message", "--behavior", "interrupt"], { from: "user" });
expect(send.opts().behavior).toBe("interrupt");
await send.parseAsync(["target", "message", "--behavior", "steer"], { from: "user" });
expect(send.opts().behavior).toBe("steer");
send.exitOverride().configureOutput({ writeErr: () => {} });
await expect(
send.parseAsync(["target", "message", "--behavior", "queue"], { from: "user" }),
).rejects.toThrow();
});
it("offers daemon host selection as a global option", () => {
expect(createCli().helpInformation()).toContain("--host <host>");
});
it("shows project, workspace, and heartbeat commands while hiding worktree compatibility", () => {
const cli = createCli();
const help = cli.helpInformation();
expect(help).toContain("project");
expect(help).toContain("workspace");
expect(help).toContain("heartbeat");
expect(help).not.toContain("worktree");
});
it("offers identical top-level and daemon config reload commands", () => {
const cli = createCli();
const reload = cli.commands.find((command) => command.name() === "reload");
const daemon = cli.commands.find((command) => command.name() === "daemon");
const nestedReload = daemon?.commands.find((command) => command.name() === "reload");
expect(reload?.helpInformation()).toContain("--host <host>");
expect(reload?.helpInformation()).toContain("--json");
expect(nestedReload?.helpInformation()).toContain("--host <host>");
expect(nestedReload?.helpInformation()).toContain("--json");
});
it("names explicit workspace creation without exposing older syntax", () => {
const run = createCli().commands.find((command) => command.name() === "run");
const help = run?.helpInformation();
expect(help).toContain("--new-workspace <local|worktree>");
expect(help).not.toContain("--isolation");
expect(help).not.toContain("--worktree <name>");
});
it("offers the worktree creation options on run", () => {
const run = createCli().commands.find((command) => command.name() === "run");
const help = run?.helpInformation();
expect(help).toContain("--worktree-mode <mode>");
expect(help).toContain("--worktree-slug <slug>");
expect(help).toContain("--new-branch <name>");
expect(help).toContain("--branch <name>");
expect(help).toContain("--pr-number <n>");
expect(help).toContain("--forge <forge>");
});
it("uses --no-wait for execution, --internal for visibility, and reserves detach for ownership", () => {
const run = createCli().commands.find((command) => command.name() === "run");
const help = run?.helpInformation();
expect(help).toContain("--no-wait");
expect(help).not.toContain("--background");
expect(help).not.toContain("--background");
expect(help).not.toContain("--detach");
});
it("hides internal agents and workspaces from listings unless --internal is passed", () => {
const cli = createCli();
const ls = cli.commands.find((command) => command.name() === "ls");
const workspace = cli.commands.find((command) => command.name() === "workspace");
const workspaceLs = workspace?.commands.find((command) => command.name() === "ls");
const workspaceCreate = workspace?.commands.find((command) => command.name() === "create");
expect(ls?.helpInformation()).toContain("--background");
expect(workspaceLs?.helpInformation()).toContain("--background");
expect(workspaceCreate?.helpInformation()).toContain("--background");
});
it("offers thinking configuration when running, updating, and scheduling agents", () => {
const cli = createCli();
const run = cli.commands.find((command) => command.name() === "run");
const agent = cli.commands.find((command) => command.name() === "agent");
const update = agent?.commands.find((command) => command.name() === "update");
const schedule = cli.commands.find((command) => command.name() === "schedule");
const scheduleCreate = schedule?.commands.find((command) => command.name() === "create");
expect(run?.helpInformation()).toContain("--thinking <id>");
expect(update?.helpInformation()).toContain("--thinking <id>");
expect(scheduleCreate?.helpInformation()).toContain("--thinking <id>");
});
it("offers opening an existing agent in the desktop app", () => {
const agent = createCli().commands.find((command) => command.name() === "agent");
const open = agent?.commands.find((command) => command.name() === "open");
expect(open?.helpInformation()).toContain("<agent-id>");
expect(open?.helpInformation()).toContain("--server <server-id>");
});
it("offers the complete local plugin lifecycle", () => {
const plugin = createCli().commands.find((command) => command.name() === "plugin");
expect(plugin?.commands.map((command) => command.name())).toEqual([
"init",
"ls",
"status",
"logs",
"install",
"update",
"reload",
"enable",
"disable",
"remove",
]);
expect(
plugin?.commands.find((command) => command.name() === "init")?.helpInformation(),
).toContain("--id <id>");
expect(
plugin?.commands.find((command) => command.name() === "install")?.helpInformation(),
).toContain("--id <id>");
});
});
it("preserves omitted and explicit false workspace background intent", () => {
const readOptions = (args: string[]) => {
const workspace = createCli().commands.find((command) => command.name() === "workspace")!;
const create = workspace.commands.find((command) => command.name() === "create")!;
create.parseOptions(args);
return create.opts();
};
expect(readOptions([]).background).toBeUndefined();
expect(readOptions(["--background"]).background).toBe(true);
expect(readOptions(["--no-background"]).background).toBe(false);
});