Files
Kilo-Org_kilocode/packages/opencode/test/kilocode/read-docx.test.ts
2026-06-08 12:04:21 +02:00

200 lines
7.2 KiB
TypeScript

import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Layer } from "effect"
import path from "path"
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"
import { Agent } from "../../src/agent/agent"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { LSP } from "../../src/lsp/lsp"
import { Instruction } from "../../src/session/instruction"
import { MessageID, SessionID } from "../../src/session/schema"
import { ReadTool } from "../../src/tool/read"
import { Tool } from "../../src/tool/tool"
import { Truncate } from "../../src/tool/truncate"
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const ctx: Tool.Context = {
sessionID: SessionID.make("ses_test-docx"),
messageID: MessageID.make("msg_test-docx"),
callID: "",
agent: "code",
abort: AbortSignal.any([]),
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
}
const expanded: Tool.Context = { ...ctx, extra: { includeDirectoryFiles: true } }
const it = testEffect(
Layer.mergeAll(
Agent.defaultLayer,
AppFileSystem.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Instruction.defaultLayer,
LSP.defaultLayer,
Truncate.defaultLayer,
),
)
const init = Effect.fn("ReadDocxTest.init")(function* () {
const info = yield* ReadTool
return yield* Tool.init(info)
})
const run = Effect.fn("ReadDocxTest.run")(function* (
args: Tool.InferParameters<typeof ReadTool>,
next: Tool.Context = ctx,
) {
const tool = yield* init()
return yield* tool.execute(args, next)
})
const exec = Effect.fn("ReadDocxTest.exec")(function* (
dir: string,
args: Tool.InferParameters<typeof ReadTool>,
next: Tool.Context = ctx,
) {
return yield* provideInstance(dir)(run(args, next))
})
const fail = Effect.fn("ReadDocxTest.fail")(function* (dir: string, args: Tool.InferParameters<typeof ReadTool>) {
const exit = yield* exec(dir, args).pipe(Effect.exit)
if (Exit.isFailure(exit)) {
const err = Cause.squash(exit.cause)
return err instanceof Error ? err : new Error(String(err))
}
throw new Error("expected read to fail")
})
const put = Effect.fn("ReadDocxTest.put")(function* (filepath: string, content: string | Uint8Array) {
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(filepath, content)
})
const document = async (paragraphs: string[], extra = "") => {
const writer = new ZipWriter(new Uint8ArrayWriter())
await writer.add(
"[Content_Types].xml",
new TextReader(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
'<Default Extension="xml" ContentType="application/xml"/>' +
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
"</Types>",
),
)
await writer.add(
"_rels/.rels",
new TextReader(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' +
"</Relationships>",
),
)
await writer.add(
"word/document.xml",
new TextReader(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>' +
paragraphs.map((text) => `<w:p><w:r><w:t>${text}</w:t></w:r></w:p>`).join("") +
extra +
"</w:body></w:document>",
),
)
return writer.close()
}
describe("kilocode DOCX reads", () => {
it.live("extracts paragraph text from .docx and .DOCX files", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const bytes = yield* Effect.promise(() => document(["First paragraph", "Second paragraph"]))
for (const ext of ["docx", "DOCX"]) {
const filepath = path.join(dir, `sample.${ext}`)
yield* put(filepath, bytes)
const result = yield* exec(dir, { filePath: filepath })
expect(result.output).toContain("1: First paragraph")
expect(result.output).toContain("Second paragraph")
expect(result.attachments).toBeUndefined()
}
}),
)
it.live("applies normal read pagination to extracted text", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const filepath = path.join(dir, "paged.docx")
yield* put(filepath, yield* Effect.promise(() => document(["First paragraph", "Second paragraph"])))
const result = yield* exec(dir, { filePath: filepath, limit: 1 })
expect(result.output).toContain("1: First paragraph")
expect(result.output).not.toContain("Second paragraph")
expect(result.output).toContain("Use offset=2")
expect(result.metadata.truncated).toBe(true)
}),
)
it.live("fails clearly for malformed DOCX files", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const filepath = path.join(dir, "invalid.docx")
yield* put(filepath, new Uint8Array([0x50, 0x4b, 0x03, 0x04]))
const err = yield* fail(dir, { filePath: filepath })
expect(err.message).toContain("Failed to extract text from DOCX file")
expect(err.message).toContain(filepath)
}),
)
it.live("includes extraction warnings for unsupported document elements", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const filepath = path.join(dir, "warning.docx")
yield* put(filepath, yield* Effect.promise(() => document(["Readable text"], "<w:unsupported/>")))
const result = yield* exec(dir, { filePath: filepath })
expect(result.output).toContain("Readable text")
expect(result.output).toContain("DOCX extraction warnings")
}),
)
it.live("does not expand DOCX content in directory reads", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const folder = path.join(dir, "folder")
yield* put(path.join(folder, "sample.docx"), yield* Effect.promise(() => document(["Hidden paragraph"])))
const result = yield* exec(dir, { filePath: folder }, expanded)
expect(result.output).toContain("sample.docx")
expect(result.output).not.toContain("Hidden paragraph")
}),
)
it.live("preserves PDF attachments and rejects unsupported binary files", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const pdf = path.join(dir, "sample.pdf")
const doc = path.join(dir, "sample.doc")
yield* put(pdf, "%PDF-1.7\nfixture")
yield* put(doc, new Uint8Array([0x00, 0x01, 0x02]))
const result = yield* exec(dir, { filePath: pdf })
const err = yield* fail(dir, { filePath: doc })
expect(result.output).toBe("PDF read successfully")
expect(result.attachments?.[0].mime).toBe("application/pdf")
expect(err.message).toContain("Cannot read binary file")
}),
)
})