mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 01:07:06 +03:00
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import path from "node:path"
|
|
import { isolatedEnv } from "../fixture/environment"
|
|
import { tmpdir } from "../fixture/tmpdir"
|
|
|
|
// A server that admits the prompt and then keeps the Session running until interrupted.
|
|
async function serve(directory: string) {
|
|
const requests: { method: string; path: string; body?: unknown }[] = []
|
|
const prompted = Promise.withResolvers<void>()
|
|
const interrupted = Promise.withResolvers<void>()
|
|
const location = { directory, project: { id: "project", directory, canonical: directory } }
|
|
const server = Bun.serve({
|
|
hostname: "127.0.0.1",
|
|
port: 0,
|
|
idleTimeout: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url)
|
|
const text = await request.text()
|
|
requests.push({ method: request.method, path: url.pathname, body: text ? JSON.parse(text) : undefined })
|
|
if (url.pathname === "/api/info")
|
|
return Response.json({ version: "test", pid: 0, urls: [], paths: { tmp: directory } })
|
|
if (request.method === "GET" && url.pathname === "/api/location") return Response.json(location)
|
|
if (request.method === "POST" && url.pathname === "/api/session")
|
|
return Response.json({
|
|
data: {
|
|
id: "ses_1",
|
|
projectID: "project",
|
|
location: { directory },
|
|
cost: 0,
|
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
time: { created: 1, updated: 1 },
|
|
},
|
|
})
|
|
if (url.pathname === "/api/event")
|
|
return new Response(
|
|
new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n`,
|
|
)
|
|
},
|
|
}),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
if (url.pathname === "/api/session/ses_1/prompt") {
|
|
prompted.resolve()
|
|
const body = JSON.parse(text)
|
|
return Response.json({
|
|
data: { id: body.id, sessionID: "ses_1", type: "user", time: { created: 1 }, payload: {}, delivery: "steer" },
|
|
})
|
|
}
|
|
if (url.pathname === "/api/session/ses_1/interrupt") {
|
|
interrupted.resolve()
|
|
return Response.json({ interrupted: true })
|
|
}
|
|
if (url.pathname.endsWith("/wait")) return interrupted.promise.then(() => new Response(null, { status: 204 }))
|
|
if (request.method === "GET") return Response.json({ data: [] })
|
|
return new Response(null, { status: 404 })
|
|
},
|
|
})
|
|
return { server, requests, prompted: prompted.promise }
|
|
}
|
|
|
|
function spawnRun(url: string, directory: string, message: string[], stdin: "pipe" | "ignore") {
|
|
return Bun.spawn([process.execPath, "run", "src/index.ts", "run", "--server", url, ...message], {
|
|
cwd: path.join(import.meta.dir, "../.."),
|
|
env: isolatedEnv(directory, { PWD: directory }),
|
|
signal: AbortSignal.timeout(25_000),
|
|
stdin,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
}
|
|
|
|
test("run sends the message as typed", async () => {
|
|
await using directory = await tmpdir()
|
|
const fake = await serve(directory.path)
|
|
const child = spawnRun(fake.server.url.toString(), directory.path, ["say hello world", `it's "quoted"`], "ignore")
|
|
try {
|
|
await fake.prompted
|
|
expect(fake.requests.find((item) => item.path.endsWith("/prompt"))?.body).toMatchObject({
|
|
text: `say hello world it's "quoted"`,
|
|
})
|
|
} finally {
|
|
child.kill("SIGKILL")
|
|
await child.exited
|
|
await fake.server.stop(true)
|
|
}
|
|
}, 30_000)
|
|
|
|
const posix = test.skipIf(process.platform === "win32")
|
|
|
|
// Windows has no SIGTERM delivery: kill() terminates the process outright.
|
|
posix(
|
|
"run does not wait on spawn's open stdin, and SIGTERM interrupts the Session with 143",
|
|
async () => {
|
|
await using directory = await tmpdir()
|
|
const fake = await serve(directory.path)
|
|
// A stdin pipe that stays open and silent, as with spawn's default stdio.
|
|
const child = spawnRun(fake.server.url.toString(), directory.path, ["hello"], "pipe")
|
|
try {
|
|
await fake.prompted
|
|
await Bun.sleep(200)
|
|
child.kill("SIGTERM")
|
|
expect(await child.exited).toBe(143)
|
|
expect(fake.requests.some((item) => item.path === "/api/session/ses_1/interrupt")).toBe(true)
|
|
} finally {
|
|
child.kill("SIGKILL")
|
|
await child.exited
|
|
await fake.server.stop(true)
|
|
}
|
|
},
|
|
30_000,
|
|
)
|
|
|
|
posix(
|
|
"run reads a slow shell pipe in full alongside a message argument",
|
|
async () => {
|
|
await using directory = await tmpdir()
|
|
const fake = await serve(directory.path)
|
|
const command = `(sleep 4; echo slow context) | "${process.execPath}" run src/index.ts run --server ${fake.server.url} review`
|
|
const child = Bun.spawn(["sh", "-c", command], {
|
|
cwd: path.join(import.meta.dir, "../.."),
|
|
env: isolatedEnv(directory.path, { PWD: directory.path }),
|
|
signal: AbortSignal.timeout(25_000),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
try {
|
|
await fake.prompted
|
|
expect(fake.requests.find((item) => item.path.endsWith("/prompt"))?.body).toMatchObject({
|
|
text: "review\nslow context\n",
|
|
})
|
|
} finally {
|
|
child.kill("SIGKILL")
|
|
await child.exited
|
|
await fake.server.stop(true)
|
|
}
|
|
},
|
|
30_000,
|
|
)
|