| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- import { expect, test } from "bun:test"
- import path from "node:path"
- import { pathToFileURL } from "node:url"
- import { tmpdir } from "../fixture/fixture"
- import run from "../../../../.opencode/tool/agentpaas_run"
- import { agents, task, upload, download } from "../../../../.opencode/tool/agentpaas"
- async function fixture(handler: (request: Request) => Response | Promise<Response>) {
- const temp = await tmpdir()
- const saved = Object.fromEntries(
- ["AGENTPAAS_URL", "AGENTPAAS_API_KEY", "AGENTPAAS_BRIDGE_STATE_DIR"].map((key) => [key, process.env[key]]),
- )
- const requests: Array<{ method: string; path: string }> = []
- const approvals: string[] = []
- const server = Bun.serve({
- hostname: "127.0.0.1",
- port: 0,
- fetch(request) {
- expect(request.headers.get("Authorization")).toBe("Bearer client-fixture")
- requests.push({ method: request.method, path: new URL(request.url).pathname })
- if (new URL(request.url).pathname.endsWith("/verify")) return Response.json({ status: "verified", checks: [] })
- return handler(request)
- },
- })
- process.env.AGENTPAAS_URL = server.url.origin
- process.env.AGENTPAAS_API_KEY = "client-fixture"
- process.env.AGENTPAAS_BRIDGE_STATE_DIR = path.join(temp.path, "state")
- return {
- path: temp.path,
- requests,
- approvals,
- context: {
- sessionID: crypto.randomUUID(),
- messageID: "m1",
- agent: "paas",
- directory: temp.path,
- worktree: temp.path,
- abort: new AbortController().signal,
- metadata() {},
- async ask(request: { permission: string }) {
- approvals.push(request.permission)
- },
- },
- async [Symbol.asyncDispose]() {
- await server.stop(true)
- for (const [key, value] of Object.entries(saved)) {
- if (value === undefined) delete process.env[key]
- else process.env[key] = value
- }
- await temp[Symbol.asyncDispose]()
- },
- }
- }
- const catalog = () =>
- Response.json({ agents: [{ id: "ag_data", name: "Data67", runtime_mode: "local" }], discovery: { status: "ok" } })
- const stream = (status: string, extra: Record<string, unknown> = {}) =>
- new Response(`event: done\ndata: ${JSON.stringify({ run_id: "run_one", status, output: "result", ...extra })}\n\n`, {
- headers: { "Content-Type": "text/event-stream" },
- })
- test("cold discovery retries reads and resolves a stable ID before submitting once", async () => {
- let discovery = 0
- await using env = await fixture((request) => {
- if (new URL(request.url).pathname === "/api/v1/agents") {
- if (++discovery === 1) return Response.json({ agents: [], discovery: { status: "refreshing" } })
- return catalog()
- }
- return stream("completed")
- })
- const result = await run.execute(
- { input: "data task", agent_id: "ag_data", mode: "iterate", conversation: "new" },
- env.context,
- )
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.metadata.status).toBe("completed")
- expect(discovery).toBe(2)
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(1)
- expect(result.metadata.research67_progress).toBeUndefined()
- })
- test("catalog preserves unavailable discovery and all agent types without executing", async () => {
- await using env = await fixture(() =>
- Response.json({ agents: [{ id: "ag_data", name: "Data67" }], discovery: { status: "unavailable" } }),
- )
- const result = await agents.execute({}, env.context)
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.output).toContain("Data67")
- expect(result.output).toContain("unavailable")
- expect(env.requests.every((item) => item.method === "GET")).toBe(true)
- })
- for (const [status, label] of [
- ["partial", "部分完成"],
- ["needs_clarification", "需要补充信息"],
- ]) {
- test(`preserves ${status} and artifacts without reporting execution failure`, async () => {
- await using env = await fixture((request) =>
- new URL(request.url).pathname === "/api/v1/agents"
- ? catalog()
- : stream(status, {
- artifacts: [{ artifact_id: "art_1", name: "report.md" }],
- }),
- )
- const result = await run.execute(
- { agent_name: "Data67", input: "analyze", mode: "iterate", conversation: "new" },
- env.context,
- )
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.title).toContain(label)
- expect(result.output).toContain("art_1")
- expect(result.output).not.toContain("执行失败")
- })
- }
- test("dropped SSE reads original run state without resubmitting or cancelling", async () => {
- await using env = await fixture(async (request) => {
- if (new URL(request.url).pathname === "/api/v1/agents") return catalog()
- if (request.method === "POST") {
- expect((await request.json()).cancel_on_disconnect).toBe(false)
- return new Response('event: started\ndata: {"run_id":"run_one","thread_id":"thread_1"}\n\n')
- }
- return Response.json({ run_id: "run_one", status: "running" })
- })
- const result = await run.execute(
- { agent_name: "Data67", input: "analyze", mode: "iterate", conversation: "new" },
- env.context,
- )
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.metadata.status).toBe("running")
- expect(result.metadata.connection_recovered).toBe(true)
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(1)
- expect(result.output).toContain("watch")
- })
- test("watch replays durable events and never submits or cancels work", async () => {
- let reads = 0
- await using env = await fixture((request) => {
- const url = new URL(request.url)
- if (url.pathname.endsWith("/events")) {
- expect(url.searchParams.get("source")).toBe("service")
- return Response.json({ events: [{ sequence: 4, event_type: "stage_completed", data: { stage_id: "analysis" } }] })
- }
- return Response.json({ run_id: "run_one", status: ++reads >= 2 ? "completed" : "running", output: "done" })
- })
- const result = await task.execute(
- { agent_id: "ag_data", run_id: "run_one", action: "watch", wait_seconds: 1 },
- env.context,
- )
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.metadata.status).toBe("completed")
- expect(result.metadata.event_cursor).toBe(4)
- expect(env.requests.every((item) => item.method === "GET")).toBe(true)
- })
- test("resume uses the existing checkpoint and one control mutation", async () => {
- let resumed = false
- await using env = await fixture(async (request) => {
- if (request.method === "POST") {
- expect(new URL(request.url).pathname).toEndWith("/run_one/resume")
- expect(await request.json()).toEqual({ checkpoint_id: "cp_original" })
- resumed = true
- }
- if (new URL(request.url).pathname.endsWith("/events")) return Response.json({ events: [] })
- return Response.json({ run_id: "run_one", status: resumed ? "running" : "paused", checkpoint_id: "cp_original" })
- })
- const result = await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "resume" }, env.context)
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(result.metadata.status).toBe("running")
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(1)
- })
- test("approval requires a user decision and binds the exact approval ID", async () => {
- let approved = false
- await using env = await fixture(async (request) => {
- if (request.method === "POST") {
- const body = await request.json()
- expect(body.approval_id).toBe("approval_1")
- expect(body.approved).toBe(true)
- expect(body.idempotency_key).toStartWith("opencode-control-")
- approved = true
- }
- if (new URL(request.url).pathname.endsWith("/events")) return Response.json({ events: [] })
- return Response.json({
- run_id: "run_one",
- status: approved ? "running" : "waiting_human",
- pending_approval: { approval_id: "approval_1", prompt: "review plan" },
- })
- })
- await expect(
- task.execute(
- { agent_id: "ag_data", run_id: "run_one", action: "approve" },
- {
- ...env.context,
- ask: async () => {
- throw new Error("user denied")
- },
- },
- ),
- ).rejects.toThrow("user denied")
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(0)
- await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "approve" }, env.context)
- expect(env.approvals).toContain("agentpaas_confirm")
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(1)
- })
- test("upload, submit references, and authenticated download preserve file bytes", async () => {
- const content = "col1,col2\n1,2\n"
- const hash = new Bun.CryptoHasher("sha256").update(content).digest("hex")
- const ref = { file_id: "file_1", name: "input.csv", status: "available", sha256: hash, size_bytes: content.length }
- await using env = await fixture(async (request) => {
- const url = new URL(request.url)
- if (url.pathname === "/api/v1/agents") return catalog()
- if (url.pathname === "/api/v1/files") {
- expect(await request.text()).toBe(content)
- expect(request.headers.get("X-File-SHA256")).toBe(hash)
- return Response.json(ref)
- }
- if (url.pathname === "/api/v1/files/capabilities")
- return Response.json({ direct_upload_max_bytes: 256 * 1024 * 1024 })
- if (url.pathname === "/api/v1/files/file_1") return Response.json(ref)
- if (url.pathname.endsWith("/run/stream")) {
- expect((await request.json()).input_files).toEqual([ref])
- return stream("completed")
- }
- if (url.pathname.endsWith("/artifacts"))
- return Response.json({
- artifacts: [{ artifact_id: "art_1", name: "../../output.csv", sha256: hash, size_bytes: content.length }],
- })
- return new Response(content)
- })
- await Bun.write(path.join(env.path, "input.csv"), content)
- const uploaded = await upload.execute({ paths: ["input.csv"] }, env.context)
- if (typeof uploaded === "string" || !uploaded.metadata) throw new Error("Expected structured tool output")
- expect(uploaded.metadata.files[0].file_id).toBe("file_1")
- await run.execute(
- { input: "analyze", agent_name: "Data67", mode: "iterate", conversation: "new", input_file_ids: ["file_1"] },
- env.context,
- )
- const result = await download.execute({ agent_id: "ag_data", run_id: "run_one", artifact_id: "art_1" }, env.context)
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured tool output")
- expect(await Bun.file(result.metadata.local_path).text()).toBe(content)
- expect(result.metadata.local_path).toStartWith(path.join(env.path, ".agentpaas-artifacts") + path.sep)
- expect(env.approvals).toEqual(["agentpaas_upload", "agentpaas_download"])
- await expect(
- download.execute({ agent_id: "ag_data", run_id: "run_one", artifact_id: "art_1" }, env.context),
- ).rejects.toThrow()
- expect(await Bun.file(result.metadata.local_path).text()).toBe(content)
- })
- test("download rejects corrupt bytes before writing", async () => {
- await using env = await fixture((request) =>
- new URL(request.url).pathname.endsWith("/artifacts")
- ? Response.json({ artifacts: [{ artifact_id: "art_1", name: "report", sha256: "0".repeat(64) }] })
- : new Response("corrupted"),
- )
- await expect(
- download.execute(
- { agent_id: "ag_data", run_id: "run_one", artifact_id: "art_1", output_path: "result.txt" },
- env.context,
- ),
- ).rejects.toThrow("摘要不匹配")
- expect(await Bun.file(path.join(env.path, "result.txt")).exists()).toBe(false)
- })
- test("failed control mutation is never retried", async () => {
- await using env = await fixture((request) =>
- request.method === "POST"
- ? Response.json({ error: "unavailable" }, { status: 503 })
- : Response.json({ run_id: "run_one", status: "running" }),
- )
- const result = await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "cancel" }, env.context)
- if (typeof result === "string") throw new Error("Expected structured result")
- expect(result.metadata?.cancellation_status).toBe("unconfirmed")
- expect(result.metadata?.status).toBe("running")
- const reread = await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "status" }, env.context)
- if (typeof reread === "string") throw new Error("Expected structured result")
- expect(reread.metadata?.cancellation_status).toBe("unconfirmed")
- expect(env.requests.filter((item) => item.method === "POST" && !item.path.endsWith("/verify"))).toHaveLength(1)
- })
- test("tool confirmation remains actionable after reconnect even while worker status is running", async () => {
- let decided = false
- await using env = await fixture(async (request) => {
- if (request.method === "POST") {
- expect((await request.json()).approved).toBe(false)
- decided = true
- }
- if (new URL(request.url).pathname.endsWith("/events")) return Response.json({ events: [] })
- return Response.json({
- run_id: "run_one",
- status: "running",
- pending_confirmation: decided
- ? null
- : { approval_id: "tc_first", status: "pending", tool: "Shell", input_text: "command" },
- })
- })
- const pending = await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "status" }, env.context)
- if (typeof pending === "string" || !pending.metadata) throw new Error("Expected structured result")
- expect(pending.metadata.status).toBe("waiting_confirmation")
- await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "reject" }, env.context)
- expect(decided).toBe(true)
- })
- test("status survives an unavailable event journal; watch returns the known result with a warning", async () => {
- await using env = await fixture((request) =>
- new URL(request.url).pathname.endsWith("/events")
- ? new Response("journal offline", { status: 503 })
- : Response.json({
- run_id: "run_old",
- session_id: "thread_old",
- status: "completed",
- output: "verified answer",
- artifacts: [{ artifact_id: "artifact_old", name: "result.csv" }],
- }),
- )
- const status = await task.execute({ agent_id: "ag_data", run_id: "run_old", action: "status" }, env.context)
- if (typeof status === "string") throw new Error("Expected structured output")
- expect(status.output).toContain("verified answer")
- expect(env.requests.some((request) => request.path.endsWith("/events"))).toBe(false)
- const watched = await task.execute(
- { agent_id: "ag_data", run_id: "run_old", action: "watch", wait_seconds: 0 },
- env.context,
- )
- if (typeof watched === "string" || !watched.metadata) throw new Error("Expected structured output")
- expect(watched.metadata.status).toBe("completed")
- expect(watched.output).toContain("artifact_old")
- expect(watched.output).toContain("事件读取暂不可用")
- })
- test("explicit historical selection persists the thread and parent for the next revision", async () => {
- let submission: Record<string, unknown> = {}
- await using env = await fixture(async (request) => {
- const route = new URL(request.url).pathname
- if (route === "/api/v1/agents") return catalog()
- if (request.method === "POST") {
- submission = await request.json()
- return stream("completed")
- }
- return Response.json({ run_id: "run_old", session_id: "thread_old", status: "completed" })
- })
- const state = path.join(env.path, "state", `${env.context.sessionID}.json`)
- await task.execute({ agent_id: "ag_data", run_id: "run_old", action: "status" }, env.context)
- expect(await Bun.file(state).exists()).toBe(false)
- await task.execute({ agent_id: "ag_data", run_id: "run_old", action: "select" }, env.context)
- expect(Object.values(await Bun.file(state).json())).toContainEqual({ thread_id: "thread_old", run_id: "run_old" })
- await run.execute(
- { agent_id: "ag_data", input: "Revise the selected research", mode: "iterate", conversation: "continue" },
- env.context,
- )
- expect(submission.thread_id).toBe("thread_old")
- expect(submission.context).toMatchObject({ run_id: "run_old" })
- expect(env.requests.filter((request) => request.method === "POST" && !request.path.endsWith("/verify"))).toHaveLength(
- 1,
- )
- })
- test("replaying resolved tool confirmations never prompts or posts another decision", async () => {
- await using env = await fixture((request) => {
- const route = new URL(request.url).pathname
- if (route === "/api/v1/agents") return catalog()
- if (route.endsWith("/run/stream"))
- return new Response(
- [
- 'event: started\ndata: {"run_id":"run_old","thread_id":"thread_old"}\n\n',
- 'event: confirm_required\ndata: {"run_id":"run_old","approval_id":"tc_old","tool":"Shell"}\n\n',
- 'event: done\ndata: {"run_id":"run_old","status":"completed","output":"done"}\n\n',
- ].join(""),
- )
- return Response.json({ run_id: "run_old", status: "completed", pending_confirmation: null })
- })
- const result = await run.execute(
- { agent_id: "ag_data", input: "existing task", mode: "iterate", conversation: "continue" },
- env.context,
- )
- if (typeof result === "string" || !result.metadata) throw new Error("Expected structured output")
- expect(result.metadata.status).toBe("completed")
- expect(env.approvals).toEqual([])
- expect(env.requests.filter((request) => request.path.endsWith("/confirm"))).toEqual([])
- })
- test("ordinary confirmation rejects stale IDs before any mutation", async () => {
- await using env = await fixture(() =>
- Response.json({
- status: "running",
- pending_confirmation: { approval_id: "tc_new", status: "pending", tool: "Shell" },
- }),
- )
- await expect(
- task.execute({ agent_id: "ag_data", run_id: "run_one", action: "approve", approval_id: "tc_old" }, env.context),
- ).rejects.toThrow("已过期")
- expect(env.requests.every((request) => request.method === "GET")).toBe(true)
- expect(env.approvals).toEqual([])
- })
- test("upload uses the server limit and refuses oversized inputs before upload", async () => {
- await using env = await fixture(() => Response.json({ direct_upload_max_bytes: 3 }))
- await Bun.write(path.join(env.path, "large.txt"), "four")
- await expect(upload.execute({ paths: ["large.txt"] }, env.context)).rejects.toThrow("3 字节")
- expect(env.requests.every((request) => request.method === "GET")).toBe(true)
- expect(env.approvals).toEqual([])
- })
- test("installer works outside source tree and preserves existing user configuration", async () => {
- await using directory = await tmpdir()
- const config = path.join(directory.path, ".opencode/opencode.jsonc")
- await Bun.write(config, '{"model":"user/model"}')
- const script = path.resolve(import.meta.dir, "../../../../script/install-agentpaas.ts")
- for (const extra of [[], ["--check"]]) {
- const child = Bun.spawn([process.execPath, script, "--target", directory.path, ...extra], {
- stdout: "pipe",
- stderr: "pipe",
- })
- expect(await child.exited, await new Response(child.stderr).text()).toBe(0)
- }
- expect(await Bun.file(config).text()).toBe('{"model":"user/model"}')
- expect(await Bun.file(path.join(directory.path, ".opencode/lib/agentpaas.ts")).exists()).toBe(true)
- const installed = await import(pathToFileURL(path.join(directory.path, ".opencode/agentpaas-manifest.json")).href)
- expect(installed.default.files).toContain("tool/agentpaas.ts")
- })
- test("lost started response is recovered by key without a second submission", async () => {
- await using env = await fixture((request) => {
- const url = new URL(request.url)
- if (url.pathname === "/api/v1/agents") return catalog()
- if (url.pathname.includes("/requests/"))
- return Response.json({ found: true, run: { run_id: "run_one", session_id: "thread_one", status: "running" } })
- return new Response("", { headers: { "Content-Type": "text/event-stream" } })
- })
- const result = await run.execute(
- { input: "task", agent_id: "ag_data", mode: "iterate", conversation: "new", background: true },
- env.context,
- )
- if (typeof result === "string") throw new Error("Expected structured result")
- expect(result.metadata?.run_id).toBe("run_one")
- expect(result.metadata?.admission_recovered).toBe(true)
- expect(env.requests.filter((row) => row.method === "POST" && !row.path.endsWith("/verify"))).toHaveLength(1)
- })
- test("pending admission survives reload and only queries the original key", async () => {
- const { pendingRequest, recoverPendingRequests } = await import("../../../../.opencode/lib/recovery")
- let found = false
- await using env = await fixture(() =>
- Response.json(
- found ? { found: true, run: { run_id: "r_recovered", status: "running" } } : { found: false, retry_safe: false },
- ),
- )
- const config = { url: process.env.AGENTPAAS_URL!, apiKey: "client-fixture" }
- await pendingRequest(config, "ag_data", "receipt_one")
- expect(await recoverPendingRequests(config, env.context.abort)).toHaveLength(1)
- found = true
- expect(await recoverPendingRequests(config, env.context.abort)).toHaveLength(0)
- const count = env.requests.length
- await recoverPendingRequests(config, env.context.abort)
- expect(env.requests).toHaveLength(count)
- expect(env.requests.every((row) => row.method === "GET")).toBe(true)
- })
- test("lost cancellation response is confirmed through authoritative snapshot", async () => {
- await using env = await fixture((request) =>
- request.method === "POST"
- ? Response.json({ error: "response lost" }, { status: 503 })
- : Response.json({ run_id: "run_one", status: "cancelled" }),
- )
- const result = await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "cancel" }, env.context)
- if (typeof result === "string") throw new Error("Expected structured result")
- expect(result.metadata?.cancellation_status).toBe("confirmed")
- expect(env.requests.filter((row) => row.method === "POST")).toHaveLength(1)
- })
- test("watch persists its event cursor and explicit after zero replays", async () => {
- const cursors: string[] = []
- await using env = await fixture((request) => {
- const url = new URL(request.url)
- if (url.pathname.endsWith("/events")) {
- cursors.push(url.searchParams.get("after")!)
- return Response.json({ events: [{ sequence: 7, event: "stage_completed", data: { stage_id: "evidence" } }] })
- }
- return Response.json({ run_id: "run_one", status: "completed" })
- })
- for (const after of [undefined, undefined, 0])
- await task.execute({ agent_id: "ag_data", run_id: "run_one", action: "watch", after }, env.context)
- expect(cursors).toEqual(["0", "7", "0"])
- })
- test("approval reminders deduplicate decisions, warn before expiry, and never mutate", async () => {
- const { approvalMonitor } = await import("../../../../.opencode/lib/approval-monitor")
- const { trackRun } = await import("../../../../.opencode/lib/agentpaas")
- let id = "approval_one",
- expires = new Date(Date.now() + 120000).toISOString()
- await using env = await fixture(() =>
- Response.json({
- status: "running",
- pending_confirmation: {
- status: "pending",
- approval_id: id,
- expires_at: expires,
- },
- }),
- )
- const config = { url: process.env.AGENTPAAS_URL!, apiKey: "client-fixture" }
- await trackRun(config, { agent_id: "ag_data", agent_name: "Data67", run_id: "run_one", thread_id: "thread_one" })
- const messages: string[] = []
- const monitor = approvalMonitor(config, async (message) => {
- messages.push(message)
- })
- try {
- await monitor.poll()
- await monitor.poll()
- expect(messages).toHaveLength(1)
- expires = new Date(Date.now() + 30000).toISOString()
- await monitor.poll()
- await monitor.poll()
- expect(messages).toHaveLength(2)
- expect(messages[1]).toContain("即将到期")
- id = "approval_two"
- await monitor.poll()
- expect(messages).toHaveLength(3)
- monitor.dispose()
- await monitor.poll()
- expect(messages).toHaveLength(3)
- expect(env.requests.every((row) => row.method === "GET")).toBe(true)
- } finally {
- monitor.dispose()
- }
- })
- test("unsupported client cost budget blocks before submitting", async () => {
- await using env = await fixture(() => catalog())
- await expect(
- run.execute(
- { input: "task", agent_id: "ag_data", mode: "iterate", conversation: "new", max_cost_usd: 0.1 },
- env.context,
- ),
- ).rejects.toThrow("未声明支持客户端费用预算")
- expect(env.requests.filter((row) => row.method === "POST" && !row.path.endsWith("/verify"))).toHaveLength(0)
- })
- test("preflight cache is short-lived and version-scoped; explicit probes bypass it", async () => {
- const { resolveAgentID, verifyAgent } = await import("../../../../.opencode/lib/agentpaas")
- let version = 1
- await using env = await fixture(() =>
- Response.json({ agents: [{ id: "ag_data", name: "Data67", current_version: version }] }),
- )
- const config = { url: process.env.AGENTPAAS_URL!, apiKey: "client-fixture" }
- await resolveAgentID(config, "ag_data", env.context.abort)
- await verifyAgent(config, "ag_data", env.context.abort, undefined, true)
- expect((await verifyAgent(config, "ag_data", env.context.abort, undefined, true)).cache_hit).toBe(true)
- await verifyAgent(config, "ag_data", env.context.abort)
- version = 2
- await resolveAgentID(config, "ag_data", env.context.abort)
- expect((await verifyAgent(config, "ag_data", env.context.abort, undefined, true)).cache_hit).toBeUndefined()
- expect(env.requests.filter((row) => row.path.endsWith("/verify"))).toHaveLength(3)
- })
- test("approval notification plugin exports a compatible lifecycle", async () => {
- const plugin = await import("../../../../.opencode/plugin/agentpaas-notify")
- expect(typeof plugin.default).toBe("function")
- })
- test("local journal failure cannot prevent cancellation reaching the server", async () => {
- const { cancelAndObserve } = await import("../../../../.opencode/lib/recovery")
- await using env = await fixture(() => Response.json({ status: "cancelled" }))
- const blocked = path.join(env.path, "not-a-directory")
- await Bun.write(blocked, "file")
- process.env.AGENTPAAS_BRIDGE_STATE_DIR = blocked
- const result = await cancelAndObserve(
- { url: process.env.AGENTPAAS_URL!, apiKey: "client-fixture" },
- "ag_data",
- "run_one",
- )
- expect(result.cancellation_status).toBe("confirmed")
- expect(result.persistence_warning).toContain("未保存")
- expect(env.requests.filter((row) => row.method === "POST")).toHaveLength(1)
- })
|