feat(aircoding): AirCoding V2 baseline — deterministic multi-agent architecture
Forked from OpenCode v1.17.4 with multi-agent system: - 5 agents: aircoding, scheduler, worker, architect, reviewer - Deterministic DAG scheduling engine (coordinator_tick) - Tool whitelists as hard enforcement - AirCoding validation plugin - System prompt injection for routing - V1 requirements: C4 docs, ADR, AGENTS.md, debug-log.md - Design documents in docs/
This commit is contained in:
259
packages/core/test/tool-webfetch.test.ts
Normal file
259
packages/core/test/tool-webfetch.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Duration, Effect, Fiber, Layer, Schema } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { WebFetchTool } from "@opencode-ai/core/tool/webfetch"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||
|
||||
const sessionID = SessionV2.ID.make("ses_webfetch_test")
|
||||
const requests: Array<{ readonly url: string; readonly headers: Record<string, string> }> = []
|
||||
const assertions: PermissionV2.AssertInput[] = []
|
||||
let respond = (_request: HttpClientRequest.HttpClientRequest) =>
|
||||
Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } }))
|
||||
|
||||
const http = Layer.succeed(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.make((request) =>
|
||||
Effect.sync(() => requests.push({ url: request.url, headers: request.headers })).pipe(
|
||||
Effect.andThen(respond(request)),
|
||||
Effect.map((response) => HttpClientResponse.fromWeb(request, response)),
|
||||
),
|
||||
),
|
||||
)
|
||||
const permission = Layer.succeed(
|
||||
PermissionV2.Service,
|
||||
PermissionV2.Service.of({
|
||||
assert: (input) => Effect.sync(() => assertions.push(input)),
|
||||
ask: () => Effect.die("unused"),
|
||||
reply: () => Effect.die("unused"),
|
||||
get: () => Effect.die("unused"),
|
||||
forSession: () => Effect.die("unused"),
|
||||
list: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(http))
|
||||
const it = testEffect(Layer.mergeAll(registry, permission, http, webfetch))
|
||||
const fetchWebfetch = WebFetchTool.layer.pipe(
|
||||
Layer.provide(registry),
|
||||
Layer.provide(permission),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, fetchWebfetch))
|
||||
|
||||
const reset = () => {
|
||||
requests.length = 0
|
||||
assertions.length = 0
|
||||
respond = () => Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } }))
|
||||
}
|
||||
|
||||
const call = (input: typeof WebFetchTool.Input.Type, id = "call-webfetch") => ({
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call" as const, id, name: "webfetch", input },
|
||||
})
|
||||
|
||||
describe("WebFetchTool helpers", () => {
|
||||
test("defaults format and rejects invalid timeout controls", () => {
|
||||
const decode = Schema.decodeUnknownSync(WebFetchTool.Input)
|
||||
expect(decode({ url: "https://example.com" })).toEqual({ url: "https://example.com", format: "markdown" })
|
||||
expect(() => decode({ url: "https://example.com", timeout: 0 })).toThrow()
|
||||
expect(() => decode({ url: "https://example.com", timeout: WebFetchTool.MAX_TIMEOUT_SECONDS + 1 })).toThrow()
|
||||
})
|
||||
|
||||
test("ports HTML text and markdown conversions without active content", () => {
|
||||
const html = "<h1>Hello</h1><script>bad()</script><p>world <strong>wide</strong></p><style>.bad {}</style>"
|
||||
expect(WebFetchTool.extractTextFromHTML(html)).toBe("Helloworld wide")
|
||||
expect(WebFetchTool.convertHTMLToMarkdown(html)).toBe("# Hello\n\nworld **wide**")
|
||||
})
|
||||
})
|
||||
|
||||
describe("WebFetchTool registration", () => {
|
||||
it.effect("registers and fetches an ordinary hostname HTTP URL without rewriting it", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const url = "http://example.com/public"
|
||||
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch"])
|
||||
expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
|
||||
result: { type: "text", value: "hello" },
|
||||
output: {
|
||||
structured: { url, contentType: "text/plain", format: "text", output: "hello" },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
})
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text", timeout: 4 } },
|
||||
])
|
||||
expect(requests).toMatchObject([{ url, headers: { accept: expect.stringContaining("text/plain;q=1.0") } }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("accepts localhost URLs with the same requested-URL permission check", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const url = "http://localhost/private"
|
||||
|
||||
expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "hello",
|
||||
})
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } },
|
||||
])
|
||||
expect(requests.map((request) => request.url)).toEqual([url])
|
||||
}),
|
||||
)
|
||||
|
||||
live.effect("follows redirects while approving only the requested URL", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() =>
|
||||
Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) =>
|
||||
new URL(request.url).pathname === "/redirect"
|
||||
? new Response("", { status: 302, headers: { location: "/target" } })
|
||||
: new Response("redirected", { headers: { "content-type": "text/plain" } }),
|
||||
}),
|
||||
),
|
||||
(server) =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const url = new URL("/redirect", server.url).toString()
|
||||
|
||||
expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "redirected",
|
||||
})
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } },
|
||||
])
|
||||
}),
|
||||
(server) => Effect.promise(() => server.stop(true)),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("rejects non-HTTP schemes before permission or transport", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* executeTool(registry, call({ url: "file:///etc/passwd", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch file:///etc/passwd",
|
||||
})
|
||||
expect(assertions).toEqual([])
|
||||
expect(requests).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("converts HTML to requested markdown and text", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
respond = () =>
|
||||
Effect.succeed(
|
||||
new Response("<h1>Hello</h1><p>world</p><script>bad()</script>", {
|
||||
headers: { "content-type": "text/html; charset=utf-8" },
|
||||
}),
|
||||
)
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toEqual({
|
||||
type: "text",
|
||||
value: "# Hello\n\nworld",
|
||||
})
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "Helloworld",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects declared and streamed oversized bodies", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
respond = () =>
|
||||
Effect.succeed(
|
||||
new Response("small", {
|
||||
headers: { "content-type": "text/plain", "content-length": String(WebFetchTool.MAX_RESPONSE_BYTES + 1) },
|
||||
}),
|
||||
)
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/declared", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/declared",
|
||||
})
|
||||
|
||||
respond = () =>
|
||||
Effect.succeed(
|
||||
new Response("x".repeat(WebFetchTool.MAX_RESPONSE_BYTES + 1), { headers: { "content-type": "text/plain" } }),
|
||||
)
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/streamed", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/streamed",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps images and files unsupported until typed settlement can carry attachments", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } }))
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/image",
|
||||
})
|
||||
|
||||
respond = () => Effect.succeed(new Response("pdf", { headers: { "content-type": "application/pdf" } }))
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/file", format: "html" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/file",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retries Cloudflare challenges with an honest user agent", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
let count = 0
|
||||
respond = () =>
|
||||
Effect.succeed(
|
||||
++count === 1
|
||||
? new Response("challenge", { status: 403, headers: { "cf-mitigated": "challenge" } })
|
||||
: new Response("ok", { headers: { "content-type": "text/plain" } }),
|
||||
)
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "ok",
|
||||
})
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[0]?.headers["user-agent"]).toContain("Mozilla/5.0")
|
||||
expect(requests[1]?.headers["user-agent"]).toBe("opencode")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("times out stalled requests", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
respond = () => Effect.never
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const fiber = yield* executeTool(
|
||||
registry,
|
||||
call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 }),
|
||||
).pipe(Effect.forkChild)
|
||||
yield* TestClock.adjust(Duration.seconds(1))
|
||||
|
||||
expect(yield* Fiber.join(fiber)).toEqual({ type: "error", value: "Unable to fetch https://1.1.1.1/slow" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user