test(mcp): hold the tool-result boundary to valid UTF-8 at every offset #276

Merged
coilysiren merged 1 commit from test/rune-boundary into main 2026-08-13 06:16:23 +00:00
Member

Tests only. Second gap from the coverage sweep that produced #275.

Why this one

runeBoundary bounds a tool result before it re-enters the prompt (mcp.go:493). It had 0% coverage.

A boundary landing mid-rune feeds the model invalid UTF-8. That is a bad failure to leave untested, because it does not surface as an error — it surfaces later as a model behaving oddly on a truncated document, which is nearly impossible to trace back.

Exhaustive rather than sampled

The input carries one, two, three, and four byte runes, and every offset from 0 to its length is checked. The whole thing is 17 bytes, so exhausting it is free, and the failure mode is a single byte in the wrong place — exactly what sampling misses.

Three properties, asserted separately because they fail differently:

Property Why it is its own assertion
the prefix is valid UTF-8 the reason the function exists
the boundary never exceeds the limit a caller slicing on it must not grow what it asked to shrink
a limit past the end clamps to the length what lets the caller slice unconditionally

Also held at 8 KiB of three-byte runes, since that is the size the tool budget actually truncates at and a 17-byte string does not exercise it.

Verified by mutation

Walking forward to the next rune start instead of back:

--- FAIL: TestRuneBoundaryNeverSplitsARune
    limit 2: boundary 3 is out of range

Caught by the never-exceed property, not the UTF-8 one — forward-walking still yields valid UTF-8, it just returns more than the caller asked for. That is why the three properties are separate assertions rather than one.

Same characterization as #275

A negative limit is returned unchanged, so the caller's slice panics. Not filed, for the same reason: no caller passes one, every site passes a constant, and a latent panic with no reachable path is tracker noise. Written down because a computed budget would find it at runtime.

That makes two truncation helpers with the same latent behaviour — truncateRunes on the Discord send path and this one on the MCP path. Worth one shared decision if anyone wants to make them total, rather than two separate fixes.

runeBoundary goes 0% → 100%. go vet, gofmt, full go test ./..., pre-commit run --files clean.


Quail (QA)

Tests only. Second gap from the coverage sweep that produced https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/275. ## Why this one `runeBoundary` bounds a tool result before it re-enters the prompt (`mcp.go:493`). It had **0% coverage**. A boundary landing mid-rune feeds the model **invalid UTF-8**. That is a bad failure to leave untested, because it does not surface as an error — it surfaces later as a model behaving oddly on a truncated document, which is nearly impossible to trace back. ## Exhaustive rather than sampled The input carries one, two, three, and four byte runes, and **every offset from 0 to its length** is checked. The whole thing is 17 bytes, so exhausting it is free, and the failure mode is a single byte in the wrong place — exactly what sampling misses. Three properties, asserted separately because they fail differently: | Property | Why it is its own assertion | | --- | --- | | the prefix is valid UTF-8 | the reason the function exists | | the boundary never exceeds the limit | a caller slicing on it must not *grow* what it asked to shrink | | a limit past the end clamps to the length | what lets the caller slice unconditionally | Also held at **8 KiB of three-byte runes**, since that is the size the tool budget actually truncates at and a 17-byte string does not exercise it. ## Verified by mutation Walking forward to the next rune start instead of back: ``` --- FAIL: TestRuneBoundaryNeverSplitsARune limit 2: boundary 3 is out of range ``` Caught by the **never-exceed** property, not the UTF-8 one — forward-walking still yields valid UTF-8, it just returns more than the caller asked for. That is why the three properties are separate assertions rather than one. ## Same characterization as #275 A negative limit is returned unchanged, so the caller's slice panics. Not filed, for the same reason: no caller passes one, every site passes a constant, and a latent panic with no reachable path is tracker noise. Written down because a computed budget would find it at runtime. That makes **two** truncation helpers with the same latent behaviour — `truncateRunes` on the Discord send path and this one on the MCP path. Worth one shared decision if anyone wants to make them total, rather than two separate fixes. `runeBoundary` goes **0% → 100%**. `go vet`, `gofmt`, full `go test ./...`, `pre-commit run --files` clean. --- Quail (QA)
test(mcp): hold the tool-result boundary to valid UTF-8 at every offset
All checks were successful
ci / test (pull_request) Successful in 28s
ci / publish-echo-image (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 18s
38361579a7
Second gap from the same coverage sweep. runeBoundary bounds a tool
result before it re-enters the prompt, and had no test. A boundary landing
mid-rune feeds the model invalid UTF-8, which is the kind of corruption
that shows up later as a model behaving oddly rather than as an error.

Exhaustive rather than sampled. The input carries one, two, three, and
four byte runes, and every offset from zero to its length is checked, so
a walk back crosses every width. The whole input is small enough to
exhaust, and the failure is a single byte in the wrong place.

Three properties, because they fail differently. The prefix stays valid
UTF-8. The boundary never exceeds the limit, so a caller slicing on it
cannot grow the value it asked to shrink. A limit past the end clamps to
the length rather than beyond it, which is what lets the caller slice
unconditionally.

Also held at the size the tool budget uses, since 8 KiB of three-byte
runes is the real case and a short string would not exercise it.

A negative limit is returned unchanged and the caller's slice panics.
Recorded as characterization for the same reason as truncateRunes: no
caller passes one, every site passes a constant, and a computed budget
would otherwise find it at runtime.

Verified by mutation. Walking forward to the next rune start instead of
back fails with "limit 2: boundary 3 is out of range", caught by the
never-exceed property rather than by the UTF-8 one.

runeBoundary goes 0% to 100%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!276
No description provided.