|
1 | 1 | import { afterEach, describe, expect } from "bun:test" |
2 | 2 | import { Cause, Effect, Exit, Layer } from "effect" |
| 3 | +import { truncate as resize } from "fs/promises" |
3 | 4 | import path from "path" |
4 | 5 | import { Agent } from "../../src/agent/agent" |
5 | 6 | import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" |
6 | 7 | import { AppFileSystem } from "../../src/filesystem" |
7 | 8 | import { FileTime } from "../../src/file/time" |
| 9 | +import { Flag } from "../../src/flag/flag" |
8 | 10 | import { LSP } from "../../src/lsp" |
9 | 11 | import { Permission } from "../../src/permission" |
10 | 12 | import { Instance } from "../../src/project/instance" |
@@ -409,6 +411,69 @@ describe("tool.read truncation", () => { |
409 | 411 | }), |
410 | 412 | ) |
411 | 413 |
|
| 414 | + it.live("attaches PDFs, video, and audio as file attachments", () => |
| 415 | + Effect.gen(function* () { |
| 416 | + const dir = yield* tmpdirScoped() |
| 417 | + const cases = [ |
| 418 | + { file: "doc.pdf", mime: "application/pdf", msg: "PDF read successfully" }, |
| 419 | + { file: "clip.mp4", mime: "video/mp4", msg: "Video read successfully" }, |
| 420 | + { file: "clip.webm", mime: "video/webm", msg: "Video read successfully" }, |
| 421 | + { file: "sound.mp3", mime: "audio/mpeg", msg: "Audio read successfully" }, |
| 422 | + ] |
| 423 | + |
| 424 | + yield* Effect.forEach( |
| 425 | + cases, |
| 426 | + (item) => |
| 427 | + Effect.gen(function* () { |
| 428 | + yield* put(path.join(dir, item.file), "media") |
| 429 | + const result = yield* exec(dir, { filePath: path.join(dir, item.file) }) |
| 430 | + |
| 431 | + expect(result.output).toBe(item.msg) |
| 432 | + expect(result.metadata.preview).toBe(item.msg) |
| 433 | + expect(result.metadata.truncated).toBe(false) |
| 434 | + expect(result.attachments).toBeDefined() |
| 435 | + expect(result.attachments?.[0].type).toBe("file") |
| 436 | + expect(result.attachments?.[0].mime).toBe(item.mime) |
| 437 | + expect(result.attachments?.[0].url).toStartWith(`data:${item.mime};base64,`) |
| 438 | + }), |
| 439 | + { concurrency: "unbounded" }, |
| 440 | + ) |
| 441 | + }), |
| 442 | + ) |
| 443 | + |
| 444 | + it.live("rejects oversized media attachments before reading content", () => |
| 445 | + Effect.gen(function* () { |
| 446 | + const dir = yield* tmpdirScoped() |
| 447 | + const file = path.join(dir, "large.mp4") |
| 448 | + yield* put(file, "") |
| 449 | + yield* Effect.promise(() => resize(file, 256 * 1024 * 1024 + 1)) |
| 450 | + |
| 451 | + const err = yield* fail(dir, { filePath: file }) |
| 452 | + expect(err.message).toContain("Cannot attach video file larger than 256.0 MB") |
| 453 | + expect(err.message).toContain(file) |
| 454 | + }), |
| 455 | + ) |
| 456 | + |
| 457 | + it.live("uses OPENCODE_READ_MAX_ATTACHMENT_BYTES for media attachment limit", () => |
| 458 | + Effect.gen(function* () { |
| 459 | + const prev = Flag.OPENCODE_READ_MAX_ATTACHMENT_BYTES |
| 460 | + try { |
| 461 | + // @ts-expect-error tests can override static env flags |
| 462 | + Flag.OPENCODE_READ_MAX_ATTACHMENT_BYTES = 4 |
| 463 | + const dir = yield* tmpdirScoped() |
| 464 | + const file = path.join(dir, "small.mp4") |
| 465 | + yield* put(file, "media") |
| 466 | + |
| 467 | + const err = yield* fail(dir, { filePath: file }) |
| 468 | + expect(err.message).toContain("Cannot attach video file larger than 4 B") |
| 469 | + expect(err.message).toContain("(5 B)") |
| 470 | + } finally { |
| 471 | + // @ts-expect-error tests can override static env flags |
| 472 | + Flag.OPENCODE_READ_MAX_ATTACHMENT_BYTES = prev |
| 473 | + } |
| 474 | + }), |
| 475 | + ) |
| 476 | + |
412 | 477 | it.live(".fbs files (FlatBuffers schema) are read as text, not images", () => |
413 | 478 | Effect.gen(function* () { |
414 | 479 | const dir = yield* tmpdirScoped() |
|
0 commit comments