Files
oh-my-opencode/src/hooks/hashline-read-enhancer/index.test.ts
T
YeonGyu-Kim 8a80b88a08 refactor(hashline): override native edit tool instead of separate tool + disabler hook
Replace 3-component hashline system (separate hashline_edit tool + edit
disabler hook + OpenAI-exempted read enhancer) with 2-component system
that directly overrides the native edit tool key, matching the
delegate_task pattern.

- Register hashline tool as 'edit' key to override native edit
- Delete hashline-edit-disabler hook (no longer needed)
- Delete hashline-provider-state module (no remaining consumers)
- Remove OpenAI exemption from read enhancer (explicit opt-in means all providers)
- Remove setProvider wiring from chat-params
2026-02-17 00:03:10 +09:00

249 lines
8.2 KiB
TypeScript

import { describe, it, expect, beforeEach } from "bun:test"
import { createHashlineReadEnhancerHook } from "./hook"
import type { PluginInput } from "@opencode-ai/plugin"
//#given - Test setup helpers
function createMockContext(): PluginInput {
return {
client: {} as unknown as PluginInput["client"],
directory: "/test",
}
}
interface TestConfig {
hashline_edit?: { enabled: boolean }
}
function createMockConfig(enabled: boolean): TestConfig {
return {
hashline_edit: { enabled },
}
}
describe("createHashlineReadEnhancerHook", () => {
let mockCtx: PluginInput
const sessionID = "test-session-123"
beforeEach(() => {
mockCtx = createMockContext()
})
describe("tool name matching", () => {
it("should process 'read' tool (lowercase)", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("1:")
expect(output.output).toContain("|")
})
it("should process 'Read' tool (mixed case)", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "Read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("|")
})
it("should process 'READ' tool (uppercase)", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "READ", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("|")
})
it("should skip non-read tools", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "edit", sessionID, callID: "call-1" }
const originalOutput = "1: hello\n2: world"
const output = { title: "Edit", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
})
describe("config flag check", () => {
it("should skip when hashline_edit is disabled", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(false))
const input = { tool: "read", sessionID, callID: "call-1" }
const originalOutput = "1: hello\n2: world"
const output = { title: "Read", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
it("should skip when hashline_edit config is missing", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, {})
const input = { tool: "read", sessionID, callID: "call-1" }
const originalOutput = "1: hello\n2: world"
const output = { title: "Read", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
})
describe("output transformation", () => {
it("should transform 'N: content' format to 'N:HASH|content'", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: function hello() {\n2: console.log('world')\n3: }", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
const lines = output.output.split("\n")
expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|function hello\(\) \{$/)
expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\| console\.log\('world'\)$/)
expect(lines[2]).toMatch(/^3:[a-f0-9]{2}\|\}$/)
})
it("should handle empty output", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe("")
})
it("should handle single line", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: const x = 1", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toMatch(/^1:[a-f0-9]{2}\|const x = 1$/)
})
})
describe("binary file detection", () => {
it("should skip binary files (no line number prefix)", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const originalOutput = "PNG\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
const output = { title: "Read", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
it("should skip if first line doesn't match pattern", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const originalOutput = "some binary data\nmore data"
const output = { title: "Read", output: originalOutput, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBe(originalOutput)
})
it("should process if first line matches 'N: ' pattern", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: valid line\n2: another line", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toContain("|")
})
})
describe("edge cases", () => {
it("should handle non-string output gracefully", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: null as unknown as string, metadata: {} }
//#when - should not throw
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toBeNull()
})
it("should handle lines with no content after colon", async () => {
//#given
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: "1: hello\n2: \n3: world", metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
const lines = output.output.split("\n")
expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|hello$/)
expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\|$/)
expect(lines[2]).toMatch(/^3:[a-f0-9]{2}\|world$/)
})
it("should handle very long lines", async () => {
//#given
const longContent = "a".repeat(1000)
const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true))
const input = { tool: "read", sessionID, callID: "call-1" }
const output = { title: "Read", output: `1: ${longContent}`, metadata: {} }
//#when
await hook["tool.execute.after"](input, output)
//#then
expect(output.output).toMatch(/^1:[a-f0-9]{2}\|a+$/)
})
})
})