mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 20:52:01 +03:00
test(cli-rs): add mini's subagent P-PTY scenarios and close M05 items
This commit is contained in:
@@ -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
@@ -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 }
|
||||
@@ -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",
|
||||
})
|
||||
@@ -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",
|
||||
})
|
||||
Reference in New Issue
Block a user