Files
opencode-plugin-dsml-fix/test/parse.test.ts
T
bendtherules 52b7776d5a feat: DSML tool-call recovery plugin for OpenCode V2
Recovers DeepSeek DSML tool calls that leak as assistant text: streaming
middleware (AI-SDK path), SSE rewrite (native path), history sanitization,
preventive directive, conditional system nudge, and a capped idle watchdog.
116 tests, synthetic golden fixtures.
2026-10-05 11:47:57 +05:30

256 lines
9.1 KiB
TypeScript

/**
* Parser tests. One `test(...)` per row in docs/TEST-MATRIX.md.
* Fixtures are built at runtime (test/fixtures.ts) so no live DSML token is stored.
*/
import { describe, expect, test } from "bun:test"
import { parseDsml, stripRanges } from "../src/parse.ts"
import { BAR, block, closeTag, eos, invoke, marker, markerAscii, markerDoubled, param, tag, text } from "./fixtures.ts"
const args = (calls: { name: string; arguments: Record<string, unknown> }[], i = 0) => calls[i]!.arguments
const one = (result: { calls: unknown[] }) => {
expect(result.calls).toHaveLength(1)
return result.calls[0] as { name: string; arguments: Record<string, unknown> }
}
describe("parseDsml — variants", () => {
test("v1-canonical", () => {
const src = text("before ", block([invoke("bash", [param("command", "ls -la")])]), " after")
const r = parseDsml(src)
expect(one(r).name).toBe("bash")
expect(args(r.calls)).toEqual({ command: "ls -la" })
expect(r.ranges).toHaveLength(1)
expect(stripRanges(src, r.ranges)).toBe("before after")
})
test("v2-doubled-bars", () => {
const m = markerDoubled()
const r = parseDsml(block([invoke("bash", [param("command", "pwd", { m })], { m })], { m }))
expect(one(r).name).toBe("bash")
})
test("v3-ascii-pipes", () => {
const m = markerAscii()
const r = parseDsml(block([invoke("bash", [param("command", "pwd", { m })], { m })], { m }))
expect(one(r).name).toBe("bash")
})
test("v4-marker-space (space before mangled tag)", () => {
// <BAR DSML BAR calls>
const open = `<${marker()} calls>`
const close = `</${marker()} calls>`
const src = open + invoke("bash", [param("command", "pwd")]) + close
const r = parseDsml(src)
expect(one(r).name).toBe("bash")
expect(stripRanges(src, r.ranges)).toBe("")
})
test("v5-truncated-tag (bare `calls`)", () => {
const r = parseDsml(block([invoke("bash", [param("command", "pwd")])], { kind: "calls" }))
expect(one(r).name).toBe("bash")
})
test("v6-name-spacing", () => {
const src = `${tag("tool_calls")}<${marker()}invoke name = "bash">${param("command", "pwd")}${closeTag("invoke")}${closeTag("tool_calls")}`
const r = parseDsml(src)
expect(one(r).name).toBe("bash")
})
test("v7-tool-call-singular", () => {
const r = parseDsml(block([invoke("read", [param("path", "a.ts")])], { kind: "tool_call" }))
expect(one(r).name).toBe("read")
})
test("v8-function-calls (V3.2)", () => {
const r = parseDsml(block([invoke("glob", [param("pattern", "*.ts")])], { kind: "function_calls" }))
expect(one(r).name).toBe("glob")
})
test("v9-orphan-invoke (no outer block)", () => {
const src = text("prose ", invoke("edit", [param("path", "Main.kt")]), " tail")
const r = parseDsml(src)
expect(one(r).name).toBe("edit")
expect(stripRanges(src, r.ranges)).toBe("prose tail")
})
test("v10-orphan-parameter (no invoke) — strip, no call", () => {
const src = text("x ", tag("parameter", 'name="edit" string="true"'), "body", closeTag("parameter"), " y")
const r = parseDsml(src)
expect(r.calls).toHaveLength(0)
expect(stripRanges(src, r.ranges)).toBe("x y")
})
test("v11-misspelled-closer does not swallow the next parameter", () => {
// <BAR DSML BAR parameter name="alpha" string="true">first</BAR DSML BAR> <BAR DSML BAR parameter name="beta" ...>second...
const a = `<${marker()}parameter name="alpha" string="true">first</${marker()}>`
const b = param("beta", "second")
const src = block([invoke("record_item", [a, b])])
const r = parseDsml(src)
expect(one(r).arguments).toEqual({ beta: "second" })
})
test("v12-runaway-name — no call, strip", () => {
const src = text("x ", tag("invoke", 'name="record_item {"'), "\ncategory: Dexes</plan>")
const r = parseDsml(src)
expect(r.calls).toHaveLength(0)
expect(r.ranges.length).toBeGreaterThan(0)
})
test("v13-undeclared-tool is kept", () => {
const r = parseDsml(block([invoke("totally_made_up", [param("x", "y")])]))
expect(one(r).name).toBe("totally_made_up")
})
test("v14-empty-name — no trap", () => {
const r = parseDsml(block([invoke("", [param("x", "y")])]))
expect(r.calls).toHaveLength(0)
})
test("v15-unclosed block flushes, keeps complete invokes", () => {
const open = tag("tool_calls")
const src = text("p ", open, invoke("bash", [param("command", "pwd")]), " trailing")
const r = parseDsml(src)
expect(one(r).name).toBe("bash")
// Everything from the opener to the end is ranged away.
expect(stripRanges(src, r.ranges)).toBe("p")
})
test("v16-truncated-invoke keeps earlier calls", () => {
const open = tag("tool_calls")
const src = text(open, invoke("a", [param("x", "1")]), tag("invoke", 'name="b"'), param("y", "2"))
const r = parseDsml(src)
expect(r.calls.map((c) => c.name)).toEqual(["a"])
})
test("v18-surrounding text preserved", () => {
const src = text("hello ", block([invoke("bash", [param("command", "pwd")])]), " world")
const r = parseDsml(src)
expect(stripRanges(src, r.ranges)).toBe("hello world")
})
test("v19-multiple invokes and blocks", () => {
const src = text(
block([invoke("a", [param("x", "1")]), invoke("b", [param("y", "2")])]),
" mid ",
block([invoke("c", [param("z", "3")]), invoke("d", [param("w", "4")])]),
)
const r = parseDsml(src)
expect(r.calls.map((c) => c.name)).toEqual(["a", "b", "c", "d"])
})
test("v20-code-fence ignored", () => {
const src = text("```\n", block([invoke("bash", [param("command", "rm -rf /")])]), "\n```")
const r = parseDsml(src)
expect(r.calls).toHaveLength(0)
expect(r.ranges).toHaveLength(0)
})
test("v21-eos stripped", () => {
const src = text(block([invoke("a", [param("x", "1")])]), eos())
const r = parseDsml(src)
expect(one(r).name).toBe("a")
expect(stripRanges(src, r.ranges)).toBe("")
})
test("v22-entities decoded", () => {
const r = parseDsml(block([invoke("bash", [param("command", "a &amp; b &quot;q&quot;")])]))
expect(one(r).arguments).toEqual({ command: 'a & b "q"' })
})
test("v23-multiline value preserved", () => {
const value = "\nline1\n line2\n"
const r = parseDsml(block([invoke("edit", [param("content", value)])]))
expect(one(r).arguments).toEqual({ content: "line1\n line2" })
})
test("v24-json parameter", () => {
const r = parseDsml(block([invoke("q", [param("questions", '[{"a":1}]', { json: true })])]))
expect(one(r).arguments).toEqual({ questions: [{ a: 1 }] })
})
test("v25-raw-json-body", () => {
const src = `${tag("tool_calls")}<${marker()}invoke name="q">{"questions":[1,2]}${closeTag("invoke")}${closeTag("tool_calls")}`
const r = parseDsml(src)
expect(one(r).arguments).toEqual({ questions: [1, 2] })
})
test("v26-wrapped-tool (markerless outer, DB 5177 shape)", () => {
const src = text(
"prose ",
`<parameter name="edit">`,
param("newString", "a"),
param("oldString", "b"),
param("path", "f.kt"),
`${closeTag("invoke")}`,
)
const r = parseDsml(src)
expect(one(r).name).toBe("edit")
expect(one(r).arguments).toEqual({ newString: "a", oldString: "b", path: "f.kt" })
expect(stripRanges(src, r.ranges)).toBe("prose")
})
test("v26-wrapped-tool with marked outer (DB 5608 shape)", () => {
const m = marker()
const src = text(
`<${m}parameter name="shell">`,
param("command", "pwd"),
`</${m}invoke>`,
)
const r = parseDsml(src)
expect(one(r).name).toBe("shell")
expect(one(r).arguments).toEqual({ command: "pwd" })
})
test("v26-plain prose parameter untouched", () => {
const src = "the <parameter> element holds a value"
const r = parseDsml(src)
expect(r.calls).toHaveLength(0)
expect(r.ranges).toHaveLength(0)
})
test("v26-nested incomplete inner → no call", () => {
const src = text(
`<parameter name="edit">`,
tag("parameter", 'name="newString" string="true"'),
"half-written",
)
const r = parseDsml(src)
expect(r.calls).toHaveLength(0)
})
})
describe("parseDsml — reject arms", () => {
test("reject-no-marker", () => {
expect(parseDsml("hello world")).toEqual({ calls: [], ranges: [] })
})
test("reject-not-dsml fast path", () => {
expect(parseDsml("a".repeat(10000))).toEqual({ calls: [], ranges: [] })
})
test("reject-empty-args", () => {
expect(parseDsml(block([invoke("bash", [])])).calls).toHaveLength(0)
})
})
describe("parseDsml — captured real shape", () => {
test("stream-multiline-edit (DB seq 5177 shape) recovers the wrapped call", () => {
// The real leak: an orphan `parameter name="edit"` wrapping complete inner
// params. V26 recovers it as a real call (V13 policy: runtime validates).
const src = text(
`\u2039/analysis\u203a\n\n`,
tag("parameter", 'name="edit"'),
"\n",
param("newString", "val x = 1"),
"\n",
param("oldString", "val x = 0"),
"\n",
closeTag("invoke"),
)
const r = parseDsml(src)
expect(r.calls).toHaveLength(1)
expect(r.calls[0]!.name).toBe("edit")
expect(stripRanges(src, r.ranges)).toBe("\u2039/analysis\u203a")
})
})