test(cli-rs): add mini's subagent P-PTY scenarios and close M05 items

This commit is contained in:
Simon Klee
2026-11-06 01:43:01 +01:00
Unverified
parent 29a69af165
commit 1ecc118a99
4 changed files with 194 additions and 19 deletions
@@ -9,26 +9,18 @@
form body, whose content does overflow, uses the scroll box's own scrollbar, which shows only on overflow as
TypeScript's form does.
## No subagent views
- TypeScript: the footer tracks child sessions (`stream-v2.subagent.ts`): the `N subagents` and `bg` hints,
`session.child.first` and its panel, the subagent inspector, `composer.subagent.interrupt`,
`session.background`, "View subagents" in the command menu, and the permissions and forms of descendant
sessions.
- Rust: none of these. A child session's permission or form does not show in mini.
- Reason: mini's transcript drops the events of other sessions, and the subagent stream is a port of its own.
It is a follow-up.
## No global forms
- TypeScript: MCP elicitations without a session (`sessionID: "global"`) of the session's location show after
the session's own forms.
- Rust: only the session's forms show.
- Reason: mini's transcript drops events of other owners; the global forms come with the subagent stream's
routing.
## No mouse in the footer
- TypeScript: hovering a permission button or a form row selects it, and a click picks it.
- TypeScript: hovering a permission button or a form row selects it, and a click picks it. In the subagent
inspector, a click on `esc back` closes it and a click on `tab next` shows the next child.
- Rust: keys only.
- Reason: mini runs with mouse tracking off (M.1), so the footer never sees the pointer.
## A child's listed permissions read their source messages
- TypeScript: listing a child's permissions reads the source message only of a request whose tool call the child's
transcript does not hold yet, and a source that cannot be read fails the list, which then retries.
- Rust: every listed request with a tool source reads its message, as the session's own permissions do, and a
source that cannot be read leaves the request without its tool input.
- Reason: the session's permissions already resolve this way (`source` in `footers/mod.rs`). The screen is the
same whenever the server answers; only extra reads differ.
+115
View File
@@ -0,0 +1,115 @@
// M.3's `ses_mini` server with two child sessions its subagent calls started: `ses_run` still runs in the
// foreground, and `ses_done` finished. The children's reads (messages, inbox, permissions, forms), the family index,
// and the writes the subagent views make (background, interrupt) are routed here.
import type { Logged, Options, Route } from "../../lib/fake-server"
import { assistant, directory, history, user } from "../M.1/fixture"
import { server as base } from "../M.3/fixture"
export { config, event, leave } from "../M.3/fixture"
const subagent = (id: string, child: string, running: boolean, agent: string, description: string) => ({
type: "tool",
id,
name: "subagent",
state: running
? { status: "running", input: { agent, description }, metadata: { sessionID: child, status: "running" } }
: {
status: "completed",
input: { agent, description },
content: [{ type: "text", text: "Read them." }],
metadata: { sessionID: child, status: "completed" },
},
time: running ? { created: 40, ran: 40 } : { created: 40, ran: 40, completed: 41 },
})
// The history, then a turn that started both children.
export const messages = [
...history,
user("msg_u4", "check the tests", 40),
assistant(
"msg_a4",
[
subagent("call_done", "ses_done", false, "general", "Read the docs"),
subagent("call_run", "ses_run", true, "explore", "Find the tests"),
],
41,
),
]
const child = (session: string) => [
{ ...user("msg_c1", "Read the docs and sum them up", 50), sessionID: session },
{
...assistant("msg_c2", [{ type: "text", text: "The docs cover setup and testing." }], 51),
sessionID: session,
},
]
type Script = Parameters<typeof base>[0] & {
// Requests the children wait on when mini opens the session.
childPermissions?: unknown[]
globalForms?: unknown[]
}
const listed = (entry: Logged, all: unknown[]) =>
entry.query.order?.[0] === "desc" ? { data: all.toReversed(), cursor: {} } : { data: all, cursor: {} }
const location = { directory }
export function server(script: Script = {}): Options {
const options = base({ messages, ...script })
const reads = (session: string, permissions: unknown[] = []): Route[] => [
{ path: `/api/session/${session}/message`, json: (entry: Logged) => listed(entry, child(session)) },
{ path: `/api/session/${session}/inbox`, json: { data: [] } },
{ path: `/api/session/${session}/permission`, json: { data: permissions } },
{ path: `/api/session/${session}/form`, json: { data: [] } },
]
const routes: Route[] = [
{ path: "/api/session/active", json: { data: { ses_run: { type: "busy" } } } },
{ path: "/api/session", json: { data: [], cursor: {} } },
{ path: "/api/form", json: { location, data: script.globalForms ?? [] } },
...reads("ses_run", script.childPermissions),
...reads("ses_done"),
{ method: "POST", path: "/api/session/ses_mini/background", status: 204 },
{ method: "POST", path: "/api/session/ses_run/interrupt", json: { interrupted: true } },
{
method: "POST",
path: "/api/session/ses_run/permission/:id/reply",
status: 204,
then: (entry: Logged) => [
{
id: "evt_childreplied0001",
created: 120,
type: "permission.replied",
durable: { aggregateID: "ses_run", seq: 1, version: 1 },
data: {
sessionID: "ses_run",
requestID: entry.path.split("/").at(-2),
reply: (entry.body as { decision: string }).decision,
},
},
],
},
{
method: "POST",
path: "/api/session/global/form/:id/reply",
status: 204,
then: (entry: Logged) => [
{
id: "evt_globalreplied0001",
created: 121,
type: "form.replied",
location,
data: {
sessionID: "global",
id: entry.path.split("/").at(-2),
answer: (entry.body as { answer: unknown }).answer,
},
},
],
},
]
return { ...options, routes: [...routes, ...(options.routes ?? [])] }
}
// The children's tabs are in: the status row counts the running one.
export const opened = { until: { screen: "1 sub" }, settle: 800, timeout: 30_000 }
+36
View File
@@ -0,0 +1,36 @@
// A running child waits on a permission and a form of no session waits at the session's location when mini opens.
// The child's permission shows first; Allow once answers it under the child's session. The global form follows;
// its answer goes to `session/global` with the location in a header, and the server's events take both away.
import { definePty } from "../../pty/run"
import { config, leave, server } from "./fixture"
const childPermission = {
id: "per_child",
sessionID: "ses_run",
action: "webfetch",
resources: ["https://example.com/tests"],
metadata: { url: "https://example.com/tests" },
}
const globalForm = {
id: "frm_global",
sessionID: "global",
title: "Connect the tracker",
fields: [{ type: "boolean", key: "connect", title: "Connect now?", required: true }],
}
export default definePty({
args: ["mini", "-s", "ses_mini"],
cols: 80,
rows: 24,
server: server({ childPermissions: [childPermission], globalForms: [globalForm] }),
setup: (root) => config(root),
steps: [
{ until: { screen: "Permission required" }, settle: 800, timeout: 30_000, checkpoint: "permission" },
{ keys: "\r", until: { screen: "Connect the tracker" }, settle: 500, checkpoint: "form" },
{ keys: "1", until: { screen: "1 sub" }, settle: 600, checkpoint: "answered" },
...leave,
],
exits: true,
requests: "writes",
})
+32
View File
@@ -0,0 +1,32 @@
// Two child sessions from the history's subagent calls: the status row counts the running one and offers to
// background it. The command menu lists them; Down on the empty prompt opens the subagent panel with the running
// child, Tab shows the finished one, and Enter inspects its transcript. Escape goes back; Ctrl+B backgrounds the
// running child (`session.background`), and its inspector's Ctrl+D interrupts it.
import { definePty } from "../../pty/run"
import { config, leave, opened, server } from "./fixture"
export default definePty({
args: ["mini", "-s", "ses_mini"],
cols: 80,
rows: 30,
server: server(),
setup: (root) => config(root),
steps: [
{ ...opened, checkpoint: "hints" },
{ keys: "\x10", until: { screen: "View subagents" }, settle: 400, checkpoint: "menu" },
{ keys: "\x1b", settle: 300 },
{ keys: "\x1b[B", until: { screen: "Select subagent" }, settle: 400, checkpoint: "panel" },
{ keys: "\t", settle: 300, checkpoint: "inactive" },
{ keys: "\r", until: { screen: "The docs cover" }, settle: 500, checkpoint: "inspector" },
{ keys: "\x1b", until: { screen: "1 sub" }, settle: 400 },
{ keys: "\x02", settle: 400 },
{ keys: "\x1b[B", until: { screen: "Select subagent" }, settle: 300 },
// The running child's spinner turns, so the screen never settles.
{ keys: "\r", until: { screen: "ctrl+d interrupt" } },
{ keys: "\x04", until: { screen: "ctrl+d interrupt" } },
{ keys: "\x1b", until: { screen: "1 sub" }, settle: 400, checkpoint: "back" },
...leave,
],
exits: true,
requests: "writes",
})