Skip to content

fix(server): steer exact id sets to bug_info - #21

Merged
plusky merged 1 commit into
mainfrom
fix/quicksearch-id-routing
Jul 28, 2026
Merged

fix(server): steer exact id sets to bug_info#21
plusky merged 1 commit into
mainfrom
fix/quicksearch-id-routing

Conversation

@plusky

@plusky plusky commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Problem

bugs_quicksearch with a comma-separated id list does content matching on every path a normal call takes: the tool prefixes the status filter (default ALL) to the query, so the expression upstream is never purely numeric and each number is full-text matched — it also matches bugs that merely mention it in their text. A reported call with 8 ids returned 14 bugs. The correct tool for an exact set of known ids is bug_info, whose envelope answers every requested id either as a bug or under restricted, instead of a search that silently drops what it may not show.

There is one exception, and the texts now say so instead of overclaiming: with an explicitly empty status the query is sent to Bugzilla bare, and Bugzilla routes a bare query of nothing but numbers to an exact id lookup (bug_id + bug_id_type=anyexact). See "The empty-status exception" below.

What changed

  • Tool descriptionsbugs_quicksearch and its query/status field docs state the status-prefixing rule, its content-matching consequence, and the empty-status exception, and steer exact id sets to bug_info (bug_ids array). quicksearch_syntax gets the matching caveat: Bugzilla's syntax page advertises a jump-to-bug-number shortcut that only the bare-query path takes.
  • Runtime advisory — when the query consists entirely of comma/whitespace-separated bug ids (optional leading # per id), the result envelope carries an extra note steering to bug_info. Its wording tracks the request: on the empty-status path it drops the content-matching claim (an id lookup is what actually happens there), and when the query names more distinct ids than bug_info's 25-per-call cap it steers to batched bug_info calls instead of straight into the too_many_ids refusal — the cap is already public in that refusal's own text and in the bug_ids parameter doc, so the note discloses nothing new.
  • Docs — the bugs_quicksearch rows in README.md and the DESIGN.md tool table tell the same story: status prefixing, content matching under any non-empty status, the bare-query id lookup, and the advisory.

The empty-status exception: accurate texts over normalization

QuicksearchParams.status accepts "", and such a call reaches the bare-query path where an all-number query IS an exact id lookup upstream. Two fixes were possible:

  • Normalize or reject status: "" so the bare path is unreachable and a categorical "never an id lookup" becomes true. Rejected: an empty status today means Bugzilla's default open-bugs search, so silently mapping "" to ALL changes result semantics for existing callers, and rejecting "" outright changes the API surface — both are behavior changes bought to keep a nicer sentence.
  • Make every text surface behaviorally accurate (chosen — most honest, zero behavior change on the search path): the content-matching claim is stated as a consequence of status prefixing with the empty-status exception named, and the runtime advisory consults p.status as well as p.query so it never asserts content matching on the id-lookup path while still steering to bug_info there. Both are client-supplied request params, so the note remains a pure function of the request; guard behavior is untouched on every path — the id-lookup rows flow through quicksearch_window → filtering → I14 link scrubbing exactly like any other search rows.

Why the advisory is safe (no new oracle)

The note is a pure function of the client's own request — id_list_advisory(&p.query, &p.status) reads nothing but bytes the client itself sent. It never consults search results, guard verdicts, or anything upstream said, so its presence and wording tell the client nothing it did not already know (I2/I3 untouched). The bugs array is byte-identical with or without it — same bugs, same order — and the query is not rerouted: a client genuinely searching for a number (an error code, a year) still gets the search it asked for. Silent rerouting was considered and rejected as a semantics break for such callers.

That property is pinned by mutation testing, not just asserted. Two oracle-shaped mutations survived the suite as it stood before the newest tests:

  • Mutation C — attach the note only when the served bugs array is non-empty. Survived because no test exercised an id-list query whose every match is policy-hidden.
  • Mutation E — suppress the note whenever I14 link scrubbing removed anything (allowed.len() != named.len() after the disclosability check). Survived because no test combined the advisory with a link-carrying projection; scrubbed ids are invisible to the client, so this would have been a genuinely covert verdict channel.

Both mutations were applied to the source one at a time, confirmed to fail the new tests, and reverted:

  • Mutation C is killed by quicksearch_advisory_survives_an_all_hidden_result (every match policy-hidden: served bugs is empty and the note must be byte-identical to the all-visible run) and quicksearch_advisory_wording_tracks_status_and_id_count (notes asserted against an upstream serving no rows at all).
  • Mutation E is killed by quicksearch_advisory_unmoved_by_link_scrubbing (a served bug's depends_on names a policy-hidden bug; the test first proves the link really is scrubbed to [], then that the note is byte-identical to the same request with the linked bug disclosable).

The other advisory tests still pin: note for pure id lists only, envelopes identical apart from the note, and a policy-hidden result row never moving the note. All five advisory mutations now live in the coverage-contract header of tools_wiremock.rs.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked (194 tests, 7 new), cargo deny check, typos — all clean.
  • Unit tests: id_list_advisory fires on pure id lists only, and its wording tracks exactly the request's status and distinct-id count (batching mentioned only above 25 distinct ids; repeated spellings of one id count once).
  • Integration tests (tools_wiremock.rs, real MCP session through the tool router): the advisory presence/byte-identity tests plus the two mutation killers and the end-to-end wording test (empty-status arm, over-cap arm).
  • Mutation evidence as above: mutations C and E each applied and confirmed to fail at least one test, then reverted; suite green after revert.

Invariants touched: I2/I3 (no new oracle — note keyed on request bytes only), I14 (scrubbing unchanged, now cross-checked against the advisory).

bugs_quicksearch prefixes the status filter (default ALL) to the query,
so under any non-empty status a query that is just a list of bug ids is
content-matched and returns every bug that merely MENTIONS one of those
numbers — a reported call with 8 ids came back with 14 bugs. With an
empty status the query goes upstream bare, and Bugzilla routes a bare
all-number query to an exact id lookup (bug_id + anyexact) instead.
Either way the right tool for an exact id set is bug_info, whose
envelope answers every requested id either as a bug or under
'restricted' instead of silently dropping it.

Steer clients there in three places:

- The bugs_quicksearch tool description and the query/status field docs
  now state the status-prefixing rule, its content-matching consequence,
  and the empty-status exception, and point exact id sets to bug_info.
  quicksearch_syntax, the README row, and the DESIGN.md row tell the
  same story, since Bugzilla's syntax page advertises a jump-to-bug-
  number shortcut that only the bare-query path takes.
- A runtime advisory: when the query consists entirely of comma/
  whitespace-separated bug ids (optional leading '#' per id), the
  result envelope carries a `note` saying so. The note is a pure
  function of the client's request — the query and status strings,
  never search results, guard verdicts, or anything upstream said — so
  it opens no new oracle (I2/I3), and the `bugs` array is byte-identical
  with or without it. On the empty-status path the note drops the
  content-matching claim (an id lookup is what actually happens there),
  and a query naming more distinct ids than bug_info's 25-per-call cap
  steers to batched bug_info calls instead of straight into the
  too_many_ids refusal — the cap is already public in that refusal's
  own text. The query is still searched, never rerouted.

Integration tests pin all of it, including the oracle edges: the note
appears for a pure id-list query, not for a content query; the served
bugs match byte-for-byte apart from the note; a policy-hidden bug, an
all-hidden (empty) result, and I14 link scrubbing each change the
served bugs but never the note; and the wording tracks only the
request's status and distinct-id count.
@plusky
plusky force-pushed the fix/quicksearch-id-routing branch from 72ca9f8 to aa82cf9 Compare July 28, 2026 08:13
@plusky
plusky merged commit 8653d56 into main Jul 28, 2026
10 checks passed
@plusky
plusky deleted the fix/quicksearch-id-routing branch July 28, 2026 08:18
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