| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- import { describe, expect, test } from "bun:test"
- import { runPromptQueue } from "@/cli/cmd/run/runtime.queue"
- import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "@/cli/cmd/run/types"
- function footer() {
- const prompts = new Set<(input: RunPrompt) => void>()
- const queuedRemoves = new Set<(messageID: string) => void>()
- const closes = new Set<() => void>()
- const events: FooterEvent[] = []
- const commits: StreamCommit[] = []
- let closed = false
- const api: FooterApi = {
- get isClosed() {
- return closed
- },
- onPrompt(fn) {
- prompts.add(fn)
- return () => {
- prompts.delete(fn)
- }
- },
- onQueuedRemove(fn) {
- queuedRemoves.add(fn)
- return () => {
- queuedRemoves.delete(fn)
- }
- },
- onClose(fn) {
- if (closed) {
- fn()
- return () => {}
- }
- closes.add(fn)
- return () => {
- closes.delete(fn)
- }
- },
- event(next) {
- events.push(next)
- },
- append(next) {
- commits.push(next)
- },
- idle() {
- return Promise.resolve()
- },
- close() {
- if (closed) {
- return
- }
- closed = true
- for (const fn of [...closes]) {
- fn()
- }
- },
- destroy() {
- api.close()
- prompts.clear()
- closes.clear()
- },
- }
- return {
- api,
- events,
- commits,
- submit(text: string, mode?: RunPrompt["mode"]) {
- const next = mode ? { text, parts: [] as RunPrompt["parts"], mode } : { text, parts: [] as RunPrompt["parts"] }
- for (const fn of [...prompts]) {
- fn(next)
- }
- },
- removeQueued(messageID: string) {
- for (const fn of [...queuedRemoves]) fn(messageID)
- },
- }
- }
- describe("run runtime queue", () => {
- test("ignores empty prompts", async () => {
- const ui = footer()
- let calls = 0
- const task = runPromptQueue({
- footer: ui.api,
- run: async () => {
- calls += 1
- },
- })
- ui.submit(" ")
- ui.api.close()
- await task
- expect(calls).toBe(0)
- })
- test("treats /exit as a close command", async () => {
- const ui = footer()
- let calls = 0
- const task = runPromptQueue({
- footer: ui.api,
- run: async () => {
- calls += 1
- },
- })
- ui.submit("/exit")
- await task
- expect(calls).toBe(0)
- })
- test("treats /new as a local session command", async () => {
- const ui = footer()
- const seen: string[] = []
- let created = 0
- const task = runPromptQueue({
- footer: ui.api,
- onNewSession: async () => {
- created += 1
- },
- run: async (input) => {
- seen.push(input.text)
- ui.api.close()
- },
- })
- ui.submit("/new")
- ui.submit("hello")
- await task
- expect(created).toBe(1)
- expect(seen).toEqual(["hello"])
- expect(ui.commits).toEqual([
- {
- kind: "user",
- text: "hello",
- phase: "start",
- source: "system",
- messageID: expect.any(String),
- },
- ])
- })
- test("shell mode submits /exit as a shell command", async () => {
- const ui = footer()
- const seen: RunPrompt[] = []
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input) => {
- seen.push(input)
- ui.api.close()
- },
- })
- ui.submit("/exit", "shell")
- await task
- expect(seen).toEqual([{ text: "/exit", parts: [], mode: "shell" }])
- expect(ui.commits).toEqual([])
- })
- test("shell mode submits /new instead of creating a session", async () => {
- const ui = footer()
- const seen: RunPrompt[] = []
- let created = 0
- const task = runPromptQueue({
- footer: ui.api,
- onNewSession: async () => {
- created += 1
- },
- run: async (input) => {
- seen.push(input)
- ui.api.close()
- },
- })
- ui.submit("/new", "shell")
- await task
- expect(created).toBe(0)
- expect(seen).toEqual([{ text: "/new", parts: [], mode: "shell" }])
- expect(ui.commits).toEqual([])
- })
- test("shell mode does not append a synthetic user row", async () => {
- const ui = footer()
- const task = runPromptQueue({
- footer: ui.api,
- run: async () => {
- expect(ui.commits).toEqual([])
- ui.api.close()
- },
- })
- ui.submit("ls", "shell")
- await task
- })
- test("preserves whitespace for initial input", async () => {
- const ui = footer()
- const seen: string[] = []
- await runPromptQueue({
- footer: ui.api,
- initialInput: " hello ",
- run: async (input) => {
- seen.push(input.text)
- ui.api.close()
- },
- })
- expect(seen).toEqual([" hello "])
- expect(ui.commits).toEqual([
- {
- kind: "user",
- text: " hello ",
- phase: "start",
- source: "system",
- messageID: expect.any(String),
- },
- ])
- })
- test("passes prompts to onSend", async () => {
- const ui = footer()
- const seen: string[] = []
- await runPromptQueue({
- footer: ui.api,
- initialInput: " hello ",
- onSend: (input) => {
- seen.push(input.text)
- },
- run: async () => {
- ui.api.close()
- },
- })
- expect(seen).toEqual([" hello "])
- })
- test("appends the user row before the turn starts", async () => {
- const ui = footer()
- await runPromptQueue({
- footer: ui.api,
- initialInput: "/fmt bash",
- run: async () => {
- expect(ui.commits).toEqual([
- {
- kind: "user",
- text: "/fmt bash",
- phase: "start",
- source: "system",
- messageID: expect.any(String),
- },
- ])
- ui.api.close()
- },
- })
- })
- test("runs queued prompts in order", async () => {
- const ui = footer()
- const seen: string[] = []
- let wake: (() => void) | undefined
- const gate = new Promise<void>((resolve) => {
- wake = resolve
- })
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input) => {
- seen.push(input.text)
- if (seen.length === 1) {
- await gate
- return
- }
- ui.api.close()
- },
- })
- ui.submit("one")
- ui.submit("two")
- await Promise.resolve()
- expect(seen).toEqual(["one"])
- wake?.()
- await task
- expect(seen).toEqual(["one", "two"])
- })
- test("exposes ordinary in-flight prompts for removal before sending", async () => {
- const ui = footer()
- const turns: RunPrompt[] = []
- let wake: (() => void) | undefined
- const gate = new Promise<void>((resolve) => {
- wake = resolve
- })
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input) => {
- turns.push(input)
- await gate
- },
- })
- ui.submit("one")
- ui.submit("two")
- await Promise.resolve()
- await Promise.resolve()
- expect(turns.map((item) => item.text)).toEqual(["one"])
- expect(turns[0]?.messageID).toEqual(expect.any(String))
- expect(ui.commits.map((item) => item.text)).toEqual(["one"])
- const first = ui.events.find((item) => item.type === "queued.prompts")
- const event = ui.events.findLast((item) => item.type === "queued.prompts")
- expect(first?.type === "queued.prompts" ? first.prompts : []).toEqual([])
- expect(
- first?.type === "queued.prompts" && event?.type === "queued.prompts" ? first.prompts === event.prompts : true,
- ).toBe(false)
- expect(ui.events.findLast((item) => item.type === "queue")).toEqual({ type: "queue", queue: 1 })
- expect(event?.type === "queued.prompts" ? event.prompts.map((item) => item.prompt.text) : []).toEqual(["two"])
- if (event?.type === "queued.prompts") ui.removeQueued(event.prompts[0]!.messageID)
- await Promise.resolve()
- wake?.()
- ui.api.close()
- await task
- expect(turns.map((item) => item.text)).toEqual(["one"])
- })
- test("removing one managed queued prompt preserves the others", async () => {
- const ui = footer()
- const turns: string[] = []
- let wake: (() => void) | undefined
- const gate = new Promise<void>((resolve) => {
- wake = resolve
- })
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input) => {
- turns.push(input.text)
- if (input.text === "active") await gate
- if (input.text === "queued three") ui.api.close()
- },
- })
- ui.submit("active")
- ui.submit("queued one")
- ui.submit("queued two")
- ui.submit("queued three")
- await Promise.resolve()
- await Promise.resolve()
- const event = ui.events.findLast((item) => item.type === "queued.prompts")
- if (event?.type === "queued.prompts") {
- const second = event.prompts.find((item) => item.prompt.text === "queued two")
- if (second) ui.removeQueued(second.messageID)
- }
- wake?.()
- await task
- expect(turns).toEqual(["active", "queued one", "queued three"])
- })
- test("drains a prompt queued during an in-flight turn", async () => {
- const ui = footer()
- const seen: string[] = []
- let wake: (() => void) | undefined
- const gate = new Promise<void>((resolve) => {
- wake = resolve
- })
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input) => {
- seen.push(input.text)
- if (seen.length === 1) {
- await gate
- return
- }
- ui.api.close()
- },
- })
- ui.submit("one")
- await Promise.resolve()
- expect(seen).toEqual(["one"])
- wake?.()
- await Promise.resolve()
- ui.submit("two")
- await task
- expect(seen).toEqual(["one", "two"])
- })
- test("close aborts the active run and drops pending queued work", async () => {
- const ui = footer()
- const seen: string[] = []
- let hit = false
- const task = runPromptQueue({
- footer: ui.api,
- run: async (input, signal) => {
- seen.push(input.text)
- await new Promise<void>((resolve) => {
- if (signal.aborted) {
- hit = true
- resolve()
- return
- }
- signal.addEventListener(
- "abort",
- () => {
- hit = true
- resolve()
- },
- { once: true },
- )
- })
- },
- })
- ui.submit("one")
- await Promise.resolve()
- ui.submit("two")
- ui.api.close()
- await task
- expect(hit).toBe(true)
- expect(seen).toEqual(["one"])
- })
- test("propagates run errors", async () => {
- const ui = footer()
- const task = runPromptQueue({
- footer: ui.api,
- run: async () => {
- throw new Error("boom")
- },
- })
- ui.submit("one")
- await expect(task).rejects.toThrow("boom")
- })
- })
|