# Test Matrix One row per parser behaviour. Each row names a fixture (inline string or file under `test/fixtures/`) and the assertion. Tests are numbered `V#`/`reject-*` to match `docs/PATTERNS.md`. Fixture convention: a **fixture is raw text built at runtime** so we can inject the real `U+FF5C` bytes without this repository ever containing them literally; helpers in `test/fixtures.ts` provide `m` (marker), `invoke`, `param`, `block`. ## Parser: `parseDsml(text)` | ID | Fixture | Expect | |----|---------|--------| | `v1-canonical` | `block([invoke("bash", [param("command","ls -la")])])` | 1 call `{name:"bash", args:{command:"ls -la"}}`, 1 range covering block | | `v2-doubled-bars` | same with doubled bars `m2` | 1 call | | `v3-ascii-pipes` | same with `|DSML|` | 1 call | | `v4-marker-space` | `< m " calls>"` (space before `calls`) | 1 call | | `v5-truncated-tag` | block tag `calls` instead of `tool_calls` | 1 call | | `v6-name-spacing` | `invoke name = "bash"` | 1 call | | `v7-tool-call-singular` | block tag `tool_call` | 1 call | | `v8-function-calls` | block tag `function_calls` | 1 call | | `v9-orphan-invoke` | `invoke("edit",[param("path","a.kt")])` with **no** block | 1 call, range covers invoke | | `v10-orphan-parameter` | `param("content","x")` only (no invoke) | 1 call named `unknown`? **NO** — see note | | `v11-misspelled-closer` | `param("alpha",..)‹BAR›>‹BAR›parameter name="beta"..` | 2 calls: alpha + beta (not swallowed) | | `v12-runaway-name` | `invoke("record_item {\\ncategory")` | 0 calls, range strips it | | `v13-undeclared-tool` | `invoke("totally_made_up",[param("x","y")])` | 1 call kept | | `v14-empty-name` | `invoke("",[param("x","y")])` | 0 calls, no infinite loop | | `v15-unclosed` | `block-open + invoke("bash",..)` with no closes | calls from complete invokes only; remainder as text | | `v16-truncated-invoke` | two invokes, 2nd missing `` | 1 call (first) | | `v18-surrounding` | `"before " + block + " after"` | text before/after preserved, block ranged | | `v19-multiple` | two invokes in one block + two blocks | 4 calls | | `v20-code-fence` | `"```\n" + block + "\n```"` | 0 calls, 0 ranges | | `v21-eos` | `block + eosMarker` | eos in ranges | | `v22-entities` | param value `a & b "q"` | decoded `a & b "q"` | | `v23-multiline-value` | `param` value with `\n` and indent | value preserved, only hug newlines removed | | `v24-json-param` | `param` `string="false"` value `{"a":1}` | args[{a:1}] as object | | `v25-raw-json-body` | invoke body `{"a":1}` no params | args `{a:1}` | | `v26-wrapped-tool` etc. | markerless/markered outer + complete inners | call X recovered; plain prose / incomplete nesting untouched | | `reject-no-marker` | `"hello world"` | 0 calls, 0 ranges | | `reject-not-dsml` | long prose, no marker | fast path (looksLikeDsml false) | | `reject-empty-args` | `invoke("bash",[])` | 0 calls (nothing to run) | **Note on `v10-orphan-parameter`:** a bare `parameter` with no enclosing invoke has no tool name, so it cannot become a call. Decision: **strip the noise, emit no call** (not `unknown`). Executing an `unknown` tool is worse than dropping an unusable fragment. This row therefore expects `calls: []`, `ranges: [the parameter markup]`. ## Streaming: `createDsmlStreamMiddleware()` | ID | Fixture | Expect | |----|---------|--------| | `stream-text-passthrough` | plain text deltas | all `text-delta` emitted, no tool parts | | `stream-v2-recovered` | doubled-bar block split per token | `tool-input-start/delta/end` + `tool-call`, no leaked text | | `stream-v17-chunk-split` | opener split `<`/`BAR`/`DSML`/… | recovered, no partial marker leaked | | `stream-finish-rewrite` | recovered call, source finish `stop` | finish `tool-calls` | | `stream-finish-untouched` | no DSML | finish stays `stop` | | `stream-unclosed-fallback` | open block, then `text-end` | buffered text emitted verbatim, finish `stop` | | `stream-empty-invoke-fallback` | closed block, unparseable | block re-emitted as text, finish `stop` | | `stream-surrounding` | `"a " + block + " b"` | text `"a b"`, 1 call | | `stream-multiline-edit` | real captured edit shape (DB V9) | 1 `edit` call with correct args | | `stream-orphan-invoke` | V9 degradation mid-stream | 1 call | | `stream-buffer-cap` | >64 KiB unclosed | fallback to text, no unbounded growth | | `stream-eager-stop` | prose opener, no structure | flushed early as text, nothing held | | `stream-transition` | reasoning part mid-text | held text flushed first, order preserved | ## Repair: `recoverStoredMessage(message)` | ID | Fixture | Expect | |----|---------|--------| | `repair-strip-only` | assistant msg with native tool-call + leaked text | text stripped, calls **not** appended | | `repair-append` | assistant msg with leaked call, no native | text stripped, call appended, `stopReason:"toolUse"` | | `repair-thinking` | leaked block in a `thinking` part | thinking stripped | | `repair-idempotent` | run twice | second run is a no-op | > Status: the repair *parser* (`parseDsml` + `stripRanges`) is fully tested. The > message-level append wrapper is intentionally **not** built: OpenCode persists > assistant messages durably and the plugin has no safe write path for them, so > repair happens at request time via history sanitization in the `context` hook > (`src/index.ts`) instead of mutating stored messages. ## Config / modes | ID | Fixture | Expect | |----|---------|--------| | `config-defaults` | `resolveConfig()` | both Zen providers, all flags on, debug off | | `config-granular` | one parse flag off | others stay on | | `config-layers` | each layer off | that layer disabled | | `parse-flags-off` | complete block + all flags off | parses | | `parse-orphan-off` | orphan invoke + flags off | no call | | `prompt-no-marker` | recovery nudge | no live marker bytes, outer-layer rule stated | | `fallback-*` | decide/messageText/send-once | nudge policy + send-once | ## Wrapper: `wrapDsmlLanguageModel` | ID | Fixture | Expect | |----|---------|--------| | `wrapper-stream` | fake model, block in stream | 1 tool-call, finish `tool-calls` | | `wrapper-passthrough` | plain text | untouched | | `wrapper-generate` | block in generate text | tool-call content part, finish `tool-calls` | ## Sources / provenance Fixtures marked with a source are byte-faithful captures from that source; others are constructed from the grammar. Provenance is recorded in `test/fixtures.ts` beside each. - `[DB]` shapes observed in a real long-context DeepSeek session: wrapped edit/shell leaks with O(1000)-char multiline values and shell metachars - `[vLLM]` vLLM #54686 leak classes (runaway name, mis-closed param, misspelled opener) - `[OpenClaw]` openclaw #128858 doubled full-width bars - `[Pi]` pi-dsml grammar (mangled ` calls>` variant) - `[CS]` CherryStudio #14747 chunk-split SSE sample