Skip to content

Commit dddd539

Browse files
authored
Merge branch 'anomalyco:dev' into dev
2 parents 48ea460 + 789705e commit dddd539

4 files changed

Lines changed: 208 additions & 6 deletions

File tree

nix/hashes.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-XIf7b6yALzH1/MkGGrsmq2DeXIC9vgD9a7D/dxhi6iU=",
4-
"aarch64-linux": "sha256-mKDCs6QhIelWc3E17zOufaSDTovtjO/Xyh3JtlWl01s=",
5-
"aarch64-darwin": "sha256-wC7bbbIyZ62uMxTr9FElTbEBMrfz0S/ndqwZZ3V9EOA=",
6-
"x86_64-darwin": "sha256-/7Nn65m5Zhvzz0TKsG9nWd2v5WDHQNi3UzCfuAR8SLo="
3+
"x86_64-linux": "sha256-saYZlUTkBfg9vp5J1CrJUM1PBXK4xKwyz28RKlT0JWo=",
4+
"aarch64-linux": "sha256-qoiX2CpOD+HSI+eLh3I84TTPdhWdG6MzfkDAXE6ldPo=",
5+
"aarch64-darwin": "sha256-LbAvdaOBuftBoHvQPFwJGr0smg8vH4wNHS6BYdyXdDs=",
6+
"x86_64-darwin": "sha256-bv5qb9Fi8SyrgZFhcdlvYNc4bjyvdyHY3YgUpmkEH2U="
77
}
88
}

packages/opencode/src/tool/webfetch.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Tool } from "./tool"
33
import TurndownService from "turndown"
44
import DESCRIPTION from "./webfetch.txt"
55
import { abortAfterAny } from "../util/abort"
6+
import { Identifier } from "../id/id"
67

78
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
89
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
@@ -87,11 +88,34 @@ export const WebFetchTool = Tool.define("webfetch", {
8788
throw new Error("Response too large (exceeds 5MB limit)")
8889
}
8990

90-
const content = new TextDecoder().decode(arrayBuffer)
9191
const contentType = response.headers.get("content-type") || ""
92-
92+
const mime = contentType.split(";")[0]?.trim().toLowerCase() || ""
9393
const title = `${params.url} (${contentType})`
9494

95+
// Check if response is an image
96+
const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet"
97+
98+
if (isImage) {
99+
const base64Content = Buffer.from(arrayBuffer).toString("base64")
100+
return {
101+
title,
102+
output: "Image fetched successfully",
103+
metadata: {},
104+
attachments: [
105+
{
106+
id: Identifier.ascending("part"),
107+
sessionID: ctx.sessionID,
108+
messageID: ctx.messageID,
109+
type: "file",
110+
mime,
111+
url: `data:${mime};base64,${base64Content}`,
112+
},
113+
],
114+
}
115+
}
116+
117+
const content = new TextDecoder().decode(arrayBuffer)
118+
95119
// Handle content based on requested format and actual content type
96120
switch (params.format) {
97121
case "markdown":

packages/opencode/test/AGENTS.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Test Fixtures Guide
2+
3+
## Temporary Directory Fixture
4+
5+
The `tmpdir` function in `fixture/fixture.ts` creates temporary directories for tests with automatic cleanup.
6+
7+
### Basic Usage
8+
9+
```typescript
10+
import { tmpdir } from "./fixture/fixture"
11+
12+
test("example", async () => {
13+
await using tmp = await tmpdir()
14+
// tmp.path is the temp directory path
15+
// automatically cleaned up when test ends
16+
})
17+
```
18+
19+
### Options
20+
21+
- `git?: boolean` - Initialize a git repo with a root commit
22+
- `config?: Partial<Config.Info>` - Write an `opencode.json` config file
23+
- `init?: (dir: string) => Promise<T>` - Custom setup function, returns value accessible as `tmp.extra`
24+
- `dispose?: (dir: string) => Promise<T>` - Custom cleanup function
25+
26+
### Examples
27+
28+
**Git repository:**
29+
30+
```typescript
31+
await using tmp = await tmpdir({ git: true })
32+
```
33+
34+
**With config file:**
35+
36+
```typescript
37+
await using tmp = await tmpdir({
38+
config: { model: "test/model", username: "testuser" },
39+
})
40+
```
41+
42+
**Custom initialization (returns extra data):**
43+
44+
```typescript
45+
await using tmp = await tmpdir<string>({
46+
init: async (dir) => {
47+
await Bun.write(path.join(dir, "file.txt"), "content")
48+
return "extra data"
49+
},
50+
})
51+
// Access extra data via tmp.extra
52+
console.log(tmp.extra) // "extra data"
53+
```
54+
55+
**With cleanup:**
56+
57+
```typescript
58+
await using tmp = await tmpdir({
59+
init: async (dir) => {
60+
const specialDir = path.join(dir, "special")
61+
await fs.mkdir(specialDir)
62+
return specialDir
63+
},
64+
dispose: async (dir) => {
65+
// Custom cleanup logic
66+
await fs.rm(path.join(dir, "special"), { recursive: true })
67+
},
68+
})
69+
```
70+
71+
### Returned Object
72+
73+
- `path: string` - Absolute path to the temp directory (realpath resolved)
74+
- `extra: T` - Value returned by the `init` function
75+
- `[Symbol.asyncDispose]` - Enables automatic cleanup via `await using`
76+
77+
### Notes
78+
79+
- Directories are created in the system temp folder with prefix `opencode-test-`
80+
- Use `await using` for automatic cleanup when the variable goes out of scope
81+
- Paths are sanitized to strip null bytes (defensive fix for CI environments)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, expect, test } from "bun:test"
2+
import path from "path"
3+
import { Instance } from "../../src/project/instance"
4+
import { WebFetchTool } from "../../src/tool/webfetch"
5+
6+
const projectRoot = path.join(import.meta.dir, "../..")
7+
8+
const ctx = {
9+
sessionID: "test",
10+
messageID: "message",
11+
callID: "",
12+
agent: "build",
13+
abort: AbortSignal.any([]),
14+
messages: [],
15+
metadata: () => {},
16+
ask: async () => {},
17+
}
18+
19+
async function withFetch(
20+
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
21+
fn: () => Promise<void>,
22+
) {
23+
const originalFetch = globalThis.fetch
24+
globalThis.fetch = mockFetch as unknown as typeof fetch
25+
try {
26+
await fn()
27+
} finally {
28+
globalThis.fetch = originalFetch
29+
}
30+
}
31+
32+
describe("tool.webfetch", () => {
33+
test("returns image responses as file attachments", async () => {
34+
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
35+
await withFetch(
36+
async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
37+
async () => {
38+
await Instance.provide({
39+
directory: projectRoot,
40+
fn: async () => {
41+
const webfetch = await WebFetchTool.init()
42+
const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx)
43+
expect(result.output).toBe("Image fetched successfully")
44+
expect(result.attachments).toBeDefined()
45+
expect(result.attachments?.length).toBe(1)
46+
expect(result.attachments?.[0].type).toBe("file")
47+
expect(result.attachments?.[0].mime).toBe("image/png")
48+
expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
49+
},
50+
})
51+
},
52+
)
53+
})
54+
55+
test("keeps svg as text output", async () => {
56+
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
57+
await withFetch(
58+
async () =>
59+
new Response(svg, {
60+
status: 200,
61+
headers: { "content-type": "image/svg+xml; charset=UTF-8" },
62+
}),
63+
async () => {
64+
await Instance.provide({
65+
directory: projectRoot,
66+
fn: async () => {
67+
const webfetch = await WebFetchTool.init()
68+
const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx)
69+
expect(result.output).toContain("<svg")
70+
expect(result.attachments).toBeUndefined()
71+
},
72+
})
73+
},
74+
)
75+
})
76+
77+
test("keeps text responses as text output", async () => {
78+
await withFetch(
79+
async () =>
80+
new Response("hello from webfetch", {
81+
status: 200,
82+
headers: { "content-type": "text/plain; charset=utf-8" },
83+
}),
84+
async () => {
85+
await Instance.provide({
86+
directory: projectRoot,
87+
fn: async () => {
88+
const webfetch = await WebFetchTool.init()
89+
const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx)
90+
expect(result.output).toBe("hello from webfetch")
91+
expect(result.attachments).toBeUndefined()
92+
},
93+
})
94+
},
95+
)
96+
})
97+
})

0 commit comments

Comments
 (0)