mirror of
https://github.com/bendtherules/opencode-plugin-dsml-fix.git
synced 2026-10-17 16:56:50 +00:00
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.
256 lines
9.1 KiB
TypeScript
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 & b "q"")])]))
|
|
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")
|
|
})
|
|
})
|