refactor(core): normalize attachment images and cache materialization per drain
This commit is contained in:
parent
28de367444
commit
6bd75aedad
3 changed files with 139 additions and 80 deletions
|
|
@ -4,6 +4,7 @@ import { pathToFileURL } from "url"
|
|||
import { DateTime, Effect, FileSystem } from "effect"
|
||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Image } from "@opencode-ai/core/image"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { FileAttachment } from "@opencode-ai/core/session/prompt"
|
||||
|
|
@ -23,11 +24,14 @@ const user = (files: FileAttachment[]) =>
|
|||
time: { created },
|
||||
})
|
||||
|
||||
// The resizer-unavailable stub exercises the raw-content fallback deterministically.
|
||||
const image = Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) })
|
||||
|
||||
const fixture = Effect.gen(function* () {
|
||||
const reader = yield* ReadToolFileSystem.Service
|
||||
const services = { reader: yield* ReadToolFileSystem.Service, image }
|
||||
const files = yield* FileSystem.FileSystem
|
||||
const directory = yield* files.makeTempDirectoryScoped()
|
||||
return { reader, files, directory }
|
||||
return { services, files, directory }
|
||||
})
|
||||
|
||||
const requireUser = (message: SessionMessage.Message) => {
|
||||
|
|
@ -38,7 +42,7 @@ const requireUser = (message: SessionMessage.Message) => {
|
|||
describe("SessionRunnerAttachment.materialize", () => {
|
||||
it.effect("expands a directory attachment into a listing instead of media", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader, files, directory } = yield* fixture
|
||||
const { services, files, directory } = yield* fixture
|
||||
yield* files.makeDirectory(path.join(directory, "src"))
|
||||
yield* files.writeFileString(path.join(directory, "package.json"), "{}")
|
||||
const attachment = FileAttachment.make({
|
||||
|
|
@ -47,7 +51,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
name: "project/",
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [user([attachment])])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [user([attachment])])
|
||||
|
||||
const message = requireUser(result[0])
|
||||
expect(message.files).toEqual([])
|
||||
|
|
@ -59,7 +63,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
|
||||
it.effect("expands a text file attachment into inline content", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader, files, directory } = yield* fixture
|
||||
const { services, files, directory } = yield* fixture
|
||||
const file = path.join(directory, "notes.md")
|
||||
yield* files.writeFileString(file, "first line\nsecond line\nthird line\n")
|
||||
const attachment = FileAttachment.make({
|
||||
|
|
@ -68,7 +72,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
name: "notes.md",
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [user([attachment])])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [user([attachment])])
|
||||
|
||||
const message = requireUser(result[0])
|
||||
expect(message.files).toEqual([])
|
||||
|
|
@ -79,7 +83,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
|
||||
it.effect("honors ?start/?end line-range parameters", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader, files, directory } = yield* fixture
|
||||
const { services, files, directory } = yield* fixture
|
||||
const file = path.join(directory, "notes.md")
|
||||
yield* files.writeFileString(file, "first line\nsecond line\nthird line\nfourth line\n")
|
||||
const attachment = FileAttachment.make({
|
||||
|
|
@ -88,7 +92,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
name: "notes.md#2-3",
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [user([attachment])])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [user([attachment])])
|
||||
|
||||
const message = requireUser(result[0])
|
||||
expect(message.text).toContain("second line")
|
||||
|
|
@ -100,7 +104,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
|
||||
it.effect("re-encodes an image attachment as a data URL media part", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader, files, directory } = yield* fixture
|
||||
const { services, files, directory } = yield* fixture
|
||||
const file = path.join(directory, "pixel.png")
|
||||
const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4])
|
||||
yield* files.writeFile(file, png)
|
||||
|
|
@ -110,7 +114,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
name: "pixel.png",
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [user([attachment])])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [user([attachment])])
|
||||
|
||||
const message = requireUser(result[0])
|
||||
expect(message.text).toBe("Look at this")
|
||||
|
|
@ -122,14 +126,14 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
|
||||
it.effect("degrades unreadable attachments to a model-visible note instead of failing", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader, directory } = yield* fixture
|
||||
const { services, directory } = yield* fixture
|
||||
const attachment = FileAttachment.make({
|
||||
uri: pathToFileURL(path.join(directory, "missing.txt")).href,
|
||||
mime: "text/plain",
|
||||
name: "missing.txt",
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [user([attachment])])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [user([attachment])])
|
||||
|
||||
const message = requireUser(result[0])
|
||||
expect(message.files).toEqual([])
|
||||
|
|
@ -137,9 +141,30 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("reuses cached materialization for the life of a drain", () =>
|
||||
Effect.gen(function* () {
|
||||
const { services, files, directory } = yield* fixture
|
||||
const file = path.join(directory, "notes.md")
|
||||
yield* files.writeFileString(file, "original content\n")
|
||||
const attachment = FileAttachment.make({
|
||||
uri: pathToFileURL(file).href,
|
||||
mime: "text/plain",
|
||||
name: "notes.md",
|
||||
})
|
||||
const cache: SessionRunnerAttachment.Cache = new Map()
|
||||
|
||||
const first = yield* SessionRunnerAttachment.materialize(services, cache, [user([attachment])])
|
||||
yield* files.writeFileString(file, "changed content\n")
|
||||
const second = yield* SessionRunnerAttachment.materialize(services, cache, [user([attachment])])
|
||||
|
||||
expect(requireUser(second[0]).text).toBe(requireUser(first[0]).text)
|
||||
expect(requireUser(second[0]).text).toContain("original content")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("passes data URLs and non-user messages through unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const { reader } = yield* fixture
|
||||
const { services } = yield* fixture
|
||||
const dataAttachment = FileAttachment.make({
|
||||
uri: "data:image/png;base64,aGVsbG8=",
|
||||
mime: "image/png",
|
||||
|
|
@ -154,7 +179,7 @@ describe("SessionRunnerAttachment.materialize", () => {
|
|||
time: { created },
|
||||
})
|
||||
|
||||
const result = yield* SessionRunnerAttachment.materialize(reader, [original, synthetic])
|
||||
const result = yield* SessionRunnerAttachment.materialize(services, new Map(), [original, synthetic])
|
||||
|
||||
expect(result[0]).toBe(original)
|
||||
expect(result[1]).toBe(synthetic)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue