Skip to content

CL-6484: adversarial model-output catalogue - #223

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-6484-adversarial
Aug 21, 2026
Merged

TheGreatAxios merged 3 commits into
mainfrom
cl-6484-adversarial

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

Adds the adversarial output catalogue to @corbits/mocks/ollama: named, composable ollama.reply scenarios reproducing specific, observed ways real local models misbehaved, plus a sequence() helper for scripting a reply per turn. Builds on unit 1 (#219) and follows its established ollama.reply.text/toolCall/toolCalls idiom exactly.

Scenarios seeded (each documents which model produced it / what broke)

  • malformedToolName() -- flagship case. qwen3.8:27b leaked \n</parameter into a tool-call function name (CL-6478). Reproduces the exact wire value the real fix (sanitizeToolNameForPersistence in vendor/intx/hub-sessions/src/sanitize-tool-name.ts) already has a regression test for.
  • toolNameOfLength(n) -- a tool-call name of exactly n characters; call with 63/64/65 to prove the boundary three shipping tools sit at against the 64-char wire cap encodeToolName enforces.
  • textlessToolCall(name, args?) -- an inference.done with no text, only a tool call -- a tool-only round, not an empty reply.
  • wrongShapedToolArgs(name, args) -- valid JSON that doesn't match the declared schema.
  • truncatedToolArgs(name, rawArguments) -- arguments that are NOT valid JSON, reaching the wire byte-for-byte (new OllamaToolCall.rawArguments override, bypassing JSON.stringify).
  • refusal(text?) -- the model declines.
  • oversized(approxChars?) -- a large text blob, finishReason: "length".
  • hallucinatedToolName() -- round 1's skills_load vs. the real load_skill.

sequence([...]) scripts one reply per turn (repeats the last once exhausted) so a multi-turn contract like "turn 1 malformed, turn 2 normal" is a one-line onChat handler.

CL-6478 regression guard

src/ollama/cl-6478-demo.test.ts demonstrates the shape: a turn assembler that persists malformedToolName()'s broken name unchanged carries the fragment straight into the next turn's history (the bug), versus a sanitized history where the room survives with a normal turn-2 reply. This is demonstrated at the mock layer only -- it does not wire into the real @workbench/hub-sessions turn assembler (vendored, out of scope for this unit's timebox). That guard already exists for the persistence step itself in vendor/intx/hub-sessions/src/sanitize-tool-name.test.ts; an equivalent test through the real turn assembler is a follow-up.

Not verified

  • No load/perf testing of oversized() at very large sizes.
  • toolNameOfLength's generated names are synthetic (namespaced_tool_probe_...), not the actual encoded form encodeToolName produces -- this exercises the wire-level character-count boundary, not the escape-scheme internals.

CL-6484 stays open -- OpenAI-compatible and Anthropic provider mocks remain.

Test plan

  • bun test in packages/mocks -- 31 pass, 0 fail
  • bunx tsc --noEmit in packages/mocks -- clean
  • bun run lint from repo root -- 0 errors (12 pre-existing warnings in unrelated files)
  • bun run check:packages from repo root -- check:packages: ok
  • Not run: full bun run check (broader test suite); not booted the product stack per instructions

DO NOT MERGE -- a peer reviewer should merge this.

Covers each seeded scenario -- malformed tool name, tool-name length
boundary, textless tool-only completion, wrong-shaped and truncated tool
arguments, refusal, oversized output, hallucinated tool name -- plus
sequence() for scripting per-turn replies, and a regression-guard demo
for CL-6478's "does the room survive the next turn?" contract.
Adds named, composable ollama.reply scenarios for the specific ways real
local models misbehaved last night, each documented with which model
produced it and what broke:

- malformedToolName() -- CL-6478's flagship case, qwen3.8:27b's
  \n</parameter leak into a function name
- toolNameOfLength(n) -- the 63/64/65-char boundary three shipping tools
  sit against (encodeToolName's 64-char wire cap)
- textlessToolCall() -- a tool-only completion with no text
- wrongShapedToolArgs() / truncatedToolArgs() -- valid-but-wrong-shape and
  unparseable tool arguments
- refusal() and oversized() -- a decline and a truncation-exercising blob
- hallucinatedToolName() -- round 1's skills_load vs. the real load_skill

OllamaToolCall gains an optional rawArguments override so a scripted
reply can put a non-JSON string on the wire byte-for-byte, and sequence()
scripts one reply per turn so multi-turn contracts (does the room survive
the turn after a malformed one?) are directly testable.
Moves the catalogue from the roadmap into the quickstart now that it
ships, with the scenario table, the sequence() multi-turn example, and a
pointer to the real CL-6478 fix in vendor/intx/hub-sessions.
@TheGreatAxios
TheGreatAxios merged commit 23ae948 into main Aug 21, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6484-adversarial branch August 25, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant