{
  "counts": {
    "confirmed": 57,
    "sequences": 33
  },
  "confirmed": [
    {
      "id": "cli-parsing-query-save-search-filter-no-conflict",
      "kind": "likely-bug",
      "title": "query save --search does not conflict with structured filter flags despite docs claiming mutual exclusivity",
      "location": "src/cli/query.rs:62-63 (and handling at src/commands/query.rs:100-130)",
      "description": "In `QueryAction::Save`, only `--from-url` carries `conflicts_with_all` against the filter flags (cli/query.rs:54). The `--search` argument (cli/query.rs:62-63) has NO conflict declaration. The doc comment for `--search` says it is 'Mutually exclusive with --from-url and the structured filter flags', and the Save long_about says 'Three mutually exclusive input modes are supported', but clap accepts `bzr query save name --search foo --product Firefox --status NEW` without error. In handle_save (commands/query.rs:100-104), `kind` is set to QueryKind::Search whenever `search.is_some()`, yet the SavedQuery is still populated with product/status/etc. (lines 107-127). The structured filters are silently stored and then ignored at run time (search-kind queries dispatch on quicksearch), so the user gets a query that does not behave as the combined flags imply. This is a missing-validation/precedence bug: either clap should reject the combination (matching the from_url treatment and the docs) or the values should be rejected in handle_save.",
      "why_uncovered": "mod_tests.rs has parse_query_save_list_kind and parse_query_save_search_kind, but neither mixes --search with filter flags. command-layer query_tests.rs covers has_filters for date-only and other queries but has no test asserting a conflict (or documented precedence) when --search is combined with --product/--status. No test exercises this branch at all.",
      "severity": "medium",
      "suggested_test": "Add a clap-parse negative test in src/cli/mod_tests.rs: `Cli::try_parse_from([\"bzr\",\"query\",\"save\",\"q\",\"--search\",\"foo\",\"--product\",\"Firefox\"])` should be Err(ArgumentConflict) (after adding `conflicts_with_all` to the `search` arg). Mirror the existing parse_set_server_*_conflicts_with_* pattern. If the design intends silent precedence instead, add a command-layer test asserting the stored SavedQuery drops the structured filters.",
      "subsystem": "cli-parsing",
      "verdict": {
        "id": "cli-parsing-query-save-search-filter-no-conflict",
        "confirmed": true,
        "reason": "Code confirms the validation/doc gap. In src/cli/query.rs, --from-url (line 54) carries conflicts_with_all against search + all filter flags, but --search (lines 62-63) has only #[arg(long)] with NO conflict declaration. Yet its doc comment (lines 58-59) says \"Mutually exclusive with --from-url and the structured filter flags\" and the Save long_about (line 11) says \"Three mutually exclusive input modes are supported.\" So clap accepts `query save name --search foo --product Firefox --status NEW` without error, contradicting the docs. In commands/query.rs handle_save (lines 100-129) kind is set to Search whenever search.is_some() while product/status/etc. are still stored.\\n\\nNo test exercises this combination. mod_tests.rs:parse_query_save_search_kind (line 1145) uses only --search + --limit; parse_query_save_list_kind (line 1110) uses only filter flags; neither mixes them. query_tests.rs has no test asserting a conflict or precedence for --search + filter flags. Functional run-tests.sh case 73 (lines 911-912) runs `query save search-bugs --search \"Bug one\" --limit 5` (no filter flags), and no functional case asserts the conflict. The path is genuinely uncovered.\\n\\nCORRECTION to the claim's stated mechanism: the description says structured filters are \"silently ignored at run time (search-kind queries dispatch on quicksearch).\" That is inaccurate. SavedQuery::into_search_params (types/bug.rs:737-763) forwards BOTH quicksearch AND every structured filter regardless of kind, and search_bugs_rest (client/bug.rs:286-288) appends multi-value, negated, and option params (including quicksearch) unconditionally with no kind-based dispatch. QueryKind::Search is used only for output labeling (output/resources/query.rs). So at run time the request carries quicksearch=foo AND product=Firefox&status=NEW together \u2014 the filters are NOT ignored, they are sent. The underlying defect (documented mutual exclusivity not enforced by clap, no test) is nonetheless real and uncovered.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "cli-parsing-bug-update-no-fields-empty-put",
      "kind": "likely-bug",
      "title": "bug update with no change flags builds an empty UpdateBugParams and PUTs it to the server",
      "location": "src/commands/bug/update.rs:99-174 (build_update_params) and update_single at ~line 192",
      "description": "`bzr bug update 42` (just an ID, no --status/--priority/--comment/list flags/etc.) parses fine (ids is the only required field) and build_update_params constructs an UpdateBugParams with every field None/empty and no comment. update_single then calls client.update_bug(42, params), sending an empty PUT. There is no guard that at least one mutating field is present, so the user gets a silent no-op API call (and a 'Updated bug #42' success message) for what is almost certainly a mistake. validate_action (update.rs:86-97) only checks the alias+multi-id case. This is missing input validation on the most basic misuse of the update command.",
      "why_uncovered": "update_tests.rs (32 tests) covers individual fields, comment handling, list-flag cleaning, and the alias-multi-id conflict, but none exercises the all-empty update. The 'Updated bug' success message is asserted only in cases that actually set a field.",
      "severity": "medium",
      "suggested_test": "Add a unit test on build_update_params (or execute) with BugAction::Update where every field is None/empty/false and assert it returns BzrError::InputValidation('no fields to update' or similar) after adding a guard. Until the guard exists, the test documents that an empty PUT is currently issued \u2014 flip the assertion once the fail-fast check lands.",
      "subsystem": "cli-parsing",
      "verdict": {
        "id": "cli-parsing-bug-update-no-fields-empty-put",
        "confirmed": true,
        "reason": "Confirmed real gap. CLI: src/cli/bug.rs:619 marks `ids` as `required = true` but defines no ArgGroup/required_unless forcing a mutating flag, so `bzr bug update 42` parses. build_update_params (src/commands/bug/update.rs:99-190) constructs UpdateBugParams with every field None/empty/Vec::new and comment None, with no all-empty guard. validate_action (update.rs:86-97) only rejects alias+multi-id. update_single (update.rs:199) calls client.update_bug, which unconditionally PUTs (src/client/bug.rs:424-426), then prints \"Updated bug #42\" (update.rs:211). No test covers this: the base helper make_update_action (update_tests.rs:10-45) always sets status+resolution, and every test including bug_update_table_output_no_comment_single (update_tests.rs:656) and bug_update_sends_put (line 191) sets at least one field; all functional cases in tests/functional/run-tests.sh (34c, 41, 42a/b, 43, 50-53f, 58, 94d-f) pass a mutating flag. The codebase already enforces the analogous guard elsewhere \u2014 query.rs:133-137 rejects \"query must have at least one filter set\" and template.rs requires a field set \u2014 so the missing bug-update guard is inconsistent with the project's own convention and is a genuine input-validation defect, not intentional. Severity medium: silent no-op API call plus a false \"Updated\" success message on basic misuse, no data loss or crash.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "cli-parsing-component-create-missing-required-no-exit2-test",
      "kind": "coverage-gap",
      "title": "No test asserts exit-code-2 (clap MissingRequiredArgument) for required-string subcommand flags",
      "location": "src/cli/component.rs:33-46 (and user.rs Create email, group add-user --group/--user)",
      "description": "Several subcommands have required (non-Option) flags: component create requires --product/--name/--description/--default-assignee (cli/component.rs:35-45), user create requires --email (cli/user.rs:56), group add-user requires --group and --user. The only negative parse tests in mod_tests.rs are the conflict cases (ArgumentConflict) and parse_unknown_command_fails. There is no test asserting that omitting a required flag yields a clap MissingRequiredArgument error (which maps to process exit code 2). The existing-coverage index claims 'exit 2 on misuse' is asserted, but the only exit-2 assertion is the description/description-file ArgumentConflict; MissingRequiredArgument is never tested for any command.",
      "why_uncovered": "parse_component_create only asserts the success path with all four flags present. No test omits a required flag. Functional tests (run-tests.sh) exercise happy paths against real Bugzilla and do not assert clap-level exit 2 for missing required args.",
      "severity": "low",
      "suggested_test": "Add mod_tests.rs cases like `Cli::try_parse_from([\"bzr\",\"component\",\"create\",\"--product\",\"P\",\"--name\",\"N\"])` and assert err.kind() == clap::error::ErrorKind::MissingRequiredArgument; similarly for `user create` without --email and `group add-user` without --user. This pins the exit-2 contract for required args, not just conflicts.",
      "subsystem": "cli-parsing",
      "verdict": {
        "id": "cli-parsing-component-create-missing-required-no-exit2-test",
        "confirmed": true,
        "reason": "Confirmed real coverage gap. No test asserts clap MissingRequiredArgument (exit 2) for any required-string subcommand flag. In src/cli/mod_tests.rs the only negative-parse tests are parse_unknown_command_fails (line 196-200, just is_err()) and three ArgumentConflict cases (lines 330-333, 444-447, 467-470); parse_component_create (line 871-885) only tests the success path with all four flags present. A repo-wide grep for MissingRequiredArgument / ErrorKind::Missing returns nothing in src or tests. In tests/functional/run-tests.sh the only assert_exit_code 2 is case 48b (line 621-627), the bug create --description/--description-file ArgumentConflict \u2014 NOT a missing-required check. The component create (case 14, line 180-181), user create (case 21, line 265-266), and group add-user (case 28, line 336-337) functional cases all supply every required flag (happy path) and never assert exit 2 for an omitted flag. Severity refined to low: the required-arg enforcement is declarative clap config (#[arg(long)] on non-Option String) that clap validates automatically; a regression would require retyping the field to Option<String>, which would surface at the execute() call site.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-bug-update-200-error-swallowed",
      "kind": "likely-bug",
      "title": "Bug.update (PUT) silently treats Bugzilla HTTP-200 error envelopes as success",
      "location": "src/client/mod.rs:239-243 (put_json), src/client/bug.rs:424-426 (update_bug)",
      "description": "update_bug delegates to put_json, which calls send(req).await? and then returns Ok(()) without ever reading or parsing the response body. The HTTP-200 Bugzilla error detection lives in check_bugzilla_200_error, which is only invoked from parse_json/parse_json_value/parse_body_to_value \u2014 none of which put_json calls. send()/check_response_status only inspects the HTTP status code (4xx/5xx). Consequently, when a Bugzilla server returns HTTP 200 with a body like {\"error\":true,\"code\":...,\"message\":\"...\"} on a Bug.update (e.g. an extension rejecting a status transition, or the documented IBM-style servers that emit error payloads at 200), bzr reports the update as succeeded, prints 'Updated bug #N', and exits 0. The very reason check_bugzilla_200_error exists (per its doc comment, IBM LTC Bugzilla) applies to mutation responses too, but the mutation path bypasses it. This also means a multi-id batch update can report all-succeeded while every update was server-rejected.",
      "why_uncovered": "update_tests.rs only mocks PUT with a healthy 200 body (mock_put_bug_ok at line 178) or an HTTP 500 (bug_update_batch_mixed_results at line 262). No test feeds a 200-status {\"error\":true,...} envelope into update_bug, so the swallowed-error path is never exercised. The GET-side 200-error handling is tested (get_bug_falls_back_on_100500) but uses parse_json, a different code path.",
      "severity": "medium",
      "suggested_test": "Add a wiremock test: mock PUT /rest/bug/42 to respond 200 with body {\"error\":true,\"code\":115,\"message\":\"You are not allowed to edit this bug\"}; call client.update_bug(42, &params) and assert it returns Err(BzrError::Api{code:115,..}) rather than Ok(()). Will fail today; fix by routing put_json through parse_json (or an explicit 200-error check on the body).",
      "subsystem": "client-bug",
      "verdict": {
        "id": "client-bug-update-200-error-swallowed",
        "confirmed": true,
        "reason": "Code confirms the gap: put_json (src/client/mod.rs:239-243) does `self.send(req).await?; Ok(())` and never reads the body; send\u2192check_response_status (mod.rs:502-531) only branches on 4xx/5xx, so HTTP 200 passes through. The 200-error detector check_bugzilla_200_error is only reached via parse_body_to_value (mod.rs:357), called solely from parse_json/parse_json_value \u2014 neither used by put_json. update_bug (src/client/bug.rs:424-426) delegates directly to put_json, so a Bug.update returning HTTP 200 {\"error\":true,...} is reported as success. Tests do not cover it: src/commands/bug/update_tests.rs PUT mocks are all healthy 200 (mock_put_bug_ok line 178-188, line 239) or HTTP 500 (bug_update_batch_mixed_results line 264); no 200-status error envelope is fed to a PUT. In src/client/bug_tests.rs every \"error\":true case (lines 183, 494, 720/731, 1202) is on a method(\"GET\") mock; get_bug_falls_back_on_100500 (line 176) is GET via parse_json. tests/functional/run-tests.sh bug update cases (41 line 460, 43 line 491) run against stock Bugzilla 5.0/5.2 which returns proper 4xx/5xx, never the IBM-style 200-error envelope. The asymmetry (GET protected, PUT not) is a genuine correctness bug.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "client-bug-hybrid-search-id-fallback-not-tested",
      "kind": "coverage-gap",
      "title": "Hybrid search XML-RPC fallback path with idless --fields normalization is untested",
      "location": "src/client/bug.rs:204-232 (search_bugs normalization + dispatch), 247-273 (search_bugs_hybrid)",
      "description": "search_bugs computes force_id_fields normalization once and passes the normalized SearchParams to all three transports. The XML-RPC normalization is tested directly via test_client_xmlrpc (xmlrpc_search_idless_include_fields_carries_id). But the hybrid path \u2014 REST returns empty with structured filters, then falls back to XML-RPC \u2014 is only tested with default fields (hybrid_search_rest_empty_with_filters_falls_back_to_xmlrpc passes product only, no --fields). There is no test that a hybrid search with include_fields=\"summary,status\" (id-less) carries the injected id all the way through the XML-RPC fallback leg. If a future refactor recomputed/forwarded the wrong params object to the fallback, deserialization of Bug.id (non-defaulted, line 55 of types/bug.rs) would fail only on the fallback leg and only in hybrid mode.",
      "why_uncovered": "Hybrid fallback tests omit --fields; the id-injection test only runs pure XML-RPC mode. The intersection (hybrid + idless fields + empty-REST fallback) is unexercised.",
      "severity": "low",
      "suggested_test": "In a hybrid client, mock REST /rest/bug -> {\"bugs\":[]} and XML-RPC to require body containing <string>id</string>,<string>summary</string>; call search_bugs with include_fields=Some(\"summary,status\") and product=[\"P\"]; assert the returned bug deserializes (id present) and the XML-RPC request carried id.",
      "subsystem": "client-bug",
      "verdict": {
        "id": "client-bug-hybrid-search-id-fallback-not-tested",
        "confirmed": true,
        "reason": "The claimed intersection is genuinely untested. (1) The only hybrid REST-empty\u2192XML-RPC-fallback search test, hybrid_search_rest_empty_with_filters_falls_back_to_xmlrpc (src/client/bug_tests.rs:303-331), passes only product:vec![\"P\"] with no include_fields; its XML-RPC mock (lines 313-320) does not assert any include_fields body, so the id-injection path on the fallback leg is not exercised. (2) The id-injection-for-XML-RPC test xmlrpc_search_idless_include_fields_carries_id (bug_tests.rs:1229-1251) uses test_client_xmlrpc (pure XML-RPC mode), not test_client_hybrid, so it never goes through search_bugs_hybrid. (3) search_bugs_prepends_id_to_idless_include_fields (bug_tests.rs:1132-1152) uses test_client (pure REST). No test combines hybrid mode + idless include_fields + empty-REST fallback. Functional tests confirm the same: tests/functional/run-tests.sh test 37 (bug list --product, line 431) and test 40 (bug search, line 445) use no --fields and no --api hybrid; the only --fields bug call (test 36, line 426, bug view --fields id,summary) already includes id and is a single-bug view, not a search; --api hybrid functional cases (lines 1059, 1105, 1311) are comment/attachment, not bug search. So the gap is real. However, severity is low: normalization is computed once in search_bugs (src/client/bug.rs:204-214) before the api_mode match and the same normalized &SearchParams reference is forwarded into search_bugs_hybrid (line 228) and from there to both search_bugs_rest (line 252) and xmlrpc.search_bugs(params) (line 261). Because the pure-XML-RPC test proves xmlrpc.search_bugs carries the injected id and the hybrid-fallback test proves the empty-REST\u2192XML-RPC dispatch and Bug.id deserialization work, the only-on-fallback regression the claim hypothesizes cannot occur without also breaking those existing tests, given the single shared params object.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-bug-get-bug-hybrid-empty-search-fallback",
      "kind": "coverage-gap",
      "title": "get_bug_rest 100500->search fallback returning an empty bugs array is untested",
      "location": "src/client/bug.rs:374-391 (get_bug_rest 100500 retry) and 393-416 (get_bug_via_search)",
      "description": "When the direct /rest/bug/{id} endpoint returns code 100500, get_bug_rest retries via get_bug_via_search. get_bug_via_search maps an empty bugs array to BzrError::NotFound (lines 409-415). In hybrid mode, get_bug's match arm only retries XML-RPC on transport failure or a *residual* 100500 Api error (lines 329-345); a NotFound bubbled up from the empty-search fallback is returned as-is (the catch-all _ arm). This is plausibly correct, but there is no test that 100500-on-direct + empty-bugs-on-search yields NotFound (and, in hybrid, does NOT trigger an XML-RPC retry). The existing get_bug_via_search_fallback_carries_id test always returns a populated bug from the search leg.",
      "why_uncovered": "get_bug_falls_back_on_100500 and get_bug_via_search_fallback_carries_id both return a non-empty bugs array from the search endpoint. hybrid_get_bug_falls_back_on_residual_100500_error returns 100500 from both legs. No test exercises 100500-then-empty (the NotFound branch of get_bug_via_search).",
      "severity": "low",
      "suggested_test": "REST mock: /rest/bug/99 -> 200 {error:true,code:100500}; /rest/bug?id=99 -> 200 {\"bugs\":[]}. Assert get_bug(\"99\",..) returns Err(BzrError::NotFound{id:\"99\",..}). Add a hybrid variant asserting the XML-RPC mock receives zero requests (NotFound must not trigger fallback).",
      "subsystem": "client-bug",
      "verdict": {
        "id": "client-bug-get-bug-hybrid-empty-search-fallback",
        "confirmed": true,
        "reason": "Verified gap is real. The three get_bug 100500-fallback tests in src/client/bug_tests.rs all return non-empty/non-NotFound from the search leg: get_bug_falls_back_on_100500 (line 176) returns a populated bug from /rest/bug?id=99; get_bug_via_search_fallback_carries_id (line 1196) returns a populated bug; hybrid_get_bug_falls_back_on_residual_100500_error (line 712) returns 100500 (not empty bugs) from search then XML-RPC. hybrid_get_bug_rest_500_falls_back_to_xmlrpc (line 463) uses HTTP 500 (transport failure), not the 100500 API-code path. No test mounts /rest/bug/{id}=100500 + /rest/bug?id={id} returning {\"bugs\":[]}, so the NotFound branch in get_bug_via_search (src/client/bug.rs:409-415) and the hybrid catch-all _ arm (src/client/bug.rs:346) that returns that NotFound without an XML-RPC retry are uncovered. tests/functional/run-tests.sh has no 100500 case (cannot reproduce an extension crash on stock Bugzilla). The behavior itself is correct/intentional per the doc comment at bug.rs:311-313, so this is a coverage gap, not a logic bug.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-bug-search-no-limit-no-param",
      "kind": "coverage-gap",
      "title": "search_bugs with limit=None omits the limit query param (server-default pagination) is untested",
      "location": "src/client/bug.rs:141-144 (append_option_params) and 296-298",
      "description": "append_option_params only appends limit when params.limit is Some. Every search/list test sets limit to Some(N) (list.rs always passes Some(*limit) from the CLI default). There is no test asserting that when limit is None, the request carries no 'limit' query param and relies on the server default. query::run and apply_overrides can leave limit=None, and a saved query with no limit flows through to search_bugs with None. If a regression made limit default to 0 or emit limit=0, the server would silently return zero rows. The boundary limit=Some(0) is also untested (Bugzilla treats limit=0 as 'no limit', which is the opposite of an empty result \u2014 a surprising semantic worth a regression test).",
      "why_uncovered": "search_bugs_sends_option_fields, search_bugs_all_fields_reach_server, etc. all set limit=Some(N). No test uses limit=None (assert absent) or limit=Some(0).",
      "severity": "low",
      "suggested_test": "Test 1: SearchParams{ limit:None, product:[\"P\"] } -> assert query_param_is_missing(\"limit\"). Test 2: SearchParams{ limit:Some(0) } -> assert query_param(\"limit\",\"0\") is sent verbatim (documents Bugzilla's no-limit semantic so a future 'treat 0 as empty' change is caught).",
      "subsystem": "client-bug",
      "verdict": {
        "id": "client-bug-search-no-limit-no-param",
        "confirmed": true,
        "reason": "Verified genuine coverage gap. In src/client/bug.rs:141-143 `append_option_params` appends the `limit` query param only when `params.limit` is `Some`, and limit is `Option<u32>` (src/types/bug.rs:116,682). No test asserts the param is absent when limit is None. The negative matcher `query_param_is_missing` IS available and used in this very file \u2014 but only for `exclude_fields` (src/client/bug_tests.rs:1156-1160 `search_bugs_strips_id_from_exclude_fields`, and tests/integration.rs:2649-2654). That test even uses `SearchParams{ exclude_fields: Some(\"id\"), ..Default::default() }` (so limit:None) yet places no constraint on `limit`; wiremock matchers are additive, so it passes whether or not a limit param is emitted. Every positive search/list/my/query test that hits a mock pins `limit` to a non-zero `Some(N)` (bug_tests.rs:214,240,791; list_tests.rs:96; my_tests.rs:74; query_tests.rs:455). The many `limit: None` cases in query_tests.rs (e.g. 682) and the query_run_applies_field_overrides test (query_tests.rs:671-678) only assert include_fields/exclude_fields params, never limit absence. No test anywhere sets `limit: Some(0)` (rg found only `limit: 50`, `Some(10)`, etc.). Functional run-tests.sh always passes `--limit N` with N>=1 (run-tests.sh:435,908,916) and only checks exit codes/output, not query params. So both the `limit=None \u2192 no param` assertion and the `limit=Some(0)` boundary are untested.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-comment-attachment-download-empty-data-string",
      "kind": "likely-bug",
      "title": "download_attachment treats data:\"\" (empty base64) as success but get_attachment_rest cannot distinguish a filtered private attachment that returns no data",
      "location": "/Users/dave/src/bzr/src/client/attachment.rs:130-139",
      "description": "download_attachment only errors with DataIntegrity(\"attachment has no data\") when `attachment.data` is None. Bugzilla 5.0.x REST, when it filters a private attachment under API-key scope, can return the attachment metadata with `data` either absent or as an empty string. If `data` is Some(\"\"), base64 decode of empty input succeeds and yields a zero-byte Vec, so download_single / write_one_attachment silently write a 0-byte file and report success \u2014 the user gets an empty file with no error and no indication the content was filtered. The None branch is the only guard. This is a silent-truncation footgun precisely in the private-attachment scenario the hybrid dispatch was added to fix.",
      "why_uncovered": "write_one_attachment_falls_back_when_data_missing covers data:Null (None -> fallback fetch). write_one_attachment_invalid_base64_returns_data_integrity covers garbage base64. No test covers data:\"\" (empty string) producing a 0-byte file, and there is no assertion that a 0-byte/empty-data result is flagged rather than silently written.",
      "severity": "medium",
      "suggested_test": "Add a unit test calling download_attachment / write_one_attachment with an Attachment whose data = Some(\"\") and size > 0, asserting the mismatch is surfaced as an error (or, if intentionally allowed, document and test that a 0-byte file is written). Also test get_attachment_rest returning data:\"\" to pin down the intended behavior.",
      "subsystem": "client-comment-attachment",
      "verdict": {
        "id": "client-comment-attachment-download-empty-data-string",
        "confirmed": true,
        "refined_severity": "medium",
        "reason": "Confirmed real, untested gap. download_attachment (src/client/attachment.rs:132-134) only guards `data == None` via `att.data.ok_or_else(...)`; `Some(\"\")` falls through, and STANDARD.decode(\"\") returns Ok(vec![]) (empty input is valid base64, not an error), so download_single (src/commands/attachment.rs:255-257) writes a 0-byte file and write_result reports \"Downloaded ... (0 bytes)\" as success. The same asymmetry exists in write_one_attachment (src/commands/attachment.rs:279-291): the `None` branch triggers a recovery re-fetch (client.download_attachment), but `Some(\"\")` takes the decode branch and silently writes 0 bytes, skipping recovery. No test exercises data:Some(\"\") \u2014 the cited tests cover only the adjacent cases: write_one_attachment_falls_back_when_data_missing (src/commands/attachment_tests.rs:724) uses data:None (make_attachment 9876 ... None at :750), and write_one_attachment_invalid_base64_returns_data_integrity (:1357) uses Some(\"not valid base64 !!\"). Grep for empty-string data across both _tests.rs files returns nothing. Functional cases 100d/100e (tests/functional/run-tests.sh:1332-1354) download a private attachment and assert the file CONTAINS \"bzr functional test content\", but only via hybrid/xmlrpc (which return real data) \u2014 there is no REST-empty-string case and no assertion that a 0-byte/empty result is flagged. The Attachment.size field (src/types/attachment.rs:36) that could detect truncation (size>0 but 0 bytes decoded) is never compared in either path. Severity medium rather than high: it is contingent on a 5.0.x REST server returning data:\"\" instead of omitting the field, and the primary private-attachment path is XML-RPC/hybrid; the code comment at attachment.rs:48-53 already notes REST truncation is \"not reliably detectable,\" so the practical exposure is the narrow REST-fallback edge."
      }
    },
    {
      "id": "client-comment-attachment-update-noop-params",
      "kind": "coverage-gap",
      "title": "update_attachment with an all-None/empty UpdateAttachmentParams sends an empty PUT body \u2014 no test guards this no-op",
      "location": "/Users/dave/src/bzr/src/client/attachment.rs:155-158, /Users/dave/src/bzr/src/types/attachment.rs:79-96",
      "description": "UpdateAttachmentParams skips every field when None/empty, so a default (all-None, no flags) params serializes to `{}`. update_attachment would then issue PUT /bug/attachment/{id} with an empty body. Bugzilla may reject or no-op this; the CLI does no validation that at least one field was supplied (unlike, e.g., the download validation). A user running `bzr attachment update <id>` with no flags gets undefined server behavior. There is no test for the empty-update case at either the client or command layer.",
      "why_uncovered": "update_attachment_sends_put and attachment_update_succeeds both supply at least one field (is_obsolete/summary). No test exercises the all-None params path or asserts the serialized body, so an empty-PUT no-op is unverified and unvalidated.",
      "severity": "low",
      "suggested_test": "Add a command-layer test running AttachmentAction::Update with all optional fields None and empty flags, asserting either an InputValidation error (nothing to update) or, if allowed, that the PUT body and resulting behavior are well-defined; and a types-level test asserting UpdateAttachmentParams::default() serializes to {}.",
      "subsystem": "client-comment-attachment",
      "verdict": {
        "id": "client-comment-attachment-update-noop-params",
        "confirmed": true,
        "reason": "The all-None/empty UpdateAttachmentParams path is genuinely untested at every layer. (1) Client test update_attachment_sends_put (src/client/attachment_tests.rs:398-401) sets is_obsolete: Some(true) and summary: Some(\"Updated patch\") \u2014 never default/all-None, and asserts nothing about the serialized body. (2) Command test attachment_update_succeeds (src/commands/attachment_tests.rs:336-338) passes summary: Some(\"Updated summary\") plus obsolete \u2014 also non-empty. (3) Functional test case 99 (tests/functional/run-tests.sh:1162-1164) runs `attachment update --summary ... --obsolete true`, again non-empty. No test builds UpdateAttachmentParams::default() nor runs `attachment update <id>` with zero flags. The serde skip_serializing_if attributes (src/types/attachment.rs:82-95) do serialize default params to `{}`, and update_attachment (src/client/attachment.rs:155-158) PUTs it with no guard; the command handler (src/commands/attachment.rs:109-118) also performs no \"at least one field\" validation. So the empty-PUT no-op is real and unverified. Severity is low: an empty `{}` PUT does not panic or corrupt data \u2014 the client returns Ok(()); this is a missing edge-case test plus a minor optional input-validation omission, not a functional defect.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-auth-version-whoami-unparseable-200",
      "kind": "coverage-gap",
      "title": "whoami 200-with-garbage-body (UnparseableResponse) branch is never exercised end-to-end",
      "location": "/Users/dave/src/bzr/src/client/auth/whoami.rs:75-77",
      "description": "probe_whoami() returns WhoamiOutcome::UnparseableResponse when a server replies 200 OK to /rest/whoami but the body is not valid whoami JSON (e.g. an HTML login page, a proxy interstitial, or a JSON object without an integer `id`). In detect_whoami_auth (whoami.rs:35-38) UnparseableResponse on the header probe causes a fall-through to the query-param probe, and in mod.rs:94 it is grouped with AuthRejected and falls through to valid_login. No test ever returns a 200 with a non-whoami body, so neither the UnparseableResponse construction nor the header->query fall-through-on-unparseable path is covered.",
      "why_uncovered": "whoami_tests.rs only unit-tests WhoamiProbeResponse deserialization (id present/zero/missing). mod_tests.rs covers 200/id>0, 200/id=0, 401, 404, and network error \u2014 but never a 200 with a body that fails serde_json::from_str::<WhoamiProbeResponse>. The id=0 test (whoami_id_zero_is_auth_rejected) exercises AuthRejected, not UnparseableResponse.",
      "severity": "low",
      "suggested_test": "Add a #[tokio::test] mounting /rest/whoami to respond 200 with set_body_string(\"<html>login</html>\") for the header probe and a 200 valid {\"id\":5} for the query-param probe; assert detect_auth_method falls through to QueryParam. Add a second test where both probes return 200 garbage and no email is supplied; assert Err with the generic 'could not authenticate' hint (mod.rs:137 _ arm).",
      "subsystem": "client-auth-version",
      "verdict": {
        "id": "client-auth-version-whoami-unparseable-200",
        "confirmed": true,
        "reason": "The WhoamiOutcome::UnparseableResponse branch (whoami.rs:77) and the header->query fall-through it triggers (whoami.rs:36) are genuinely uncovered. Verification of every whoami 200 test path: (1) whoami_tests.rs:5-24 only calls serde_json::from_str on WhoamiProbeResponse directly (id present/zero/missing) and never invokes probe_whoami/detect_whoami_auth over the network. The missing-id case (whoami_response_missing_id_defaults_zero, line 19) parses successfully to id=0 because of #[serde(default)], so even at the network layer it would hit AuthRejected (whoami.rs:73), not UnparseableResponse. (2) In mod_tests.rs every 200 sent to /rest/whoami carries valid whoami JSON: header_auth_succeeds {\"id\":42} (line 16), falls_back_to_query_param {\"id\":7} on query, 401 on header (line 33/40), whoami_id_zero_is_auth_rejected {\"id\":0} (line 299, hits AuthRejected per whoami.rs:73 not Unparseable), and detect_server_settings_* {\"id\":1} (lines 314/363). The only set_body_string(\"not json\") at mod_tests.rs:370 is on /rest/version, not /rest/whoami. (3) src/commands/whoami_tests.rs uses a 500 status. (4) integration.rs:289 and :2039 use valid JSON. (5) tests/functional/run-tests.sh:131-136 and :1403 run `bzr whoami` against a live container returning valid JSON. The literal \"UnparseableResponse\" appears only in production code (whoami.rs, mod.rs), in no test. No test ever returns a 200 with a body that fails serde_json::from_str::<WhoamiProbeResponse> (invalid JSON / non-integer id), so neither the Unparseable construction nor the on-Unparseable header->query fall-through is exercised end-to-end.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-auth-version-validlogin-network-error",
      "kind": "coverage-gap",
      "title": "valid_login NetworkError fallback to Header auth is untested",
      "location": "/Users/dave/src/bzr/src/client/auth/mod.rs:116-122",
      "description": "When whoami returns 404 (NotFound), an email is present, and the valid_login probe then hits a network/TLS error, detect_valid_login_auth returns ValidLoginOutcome::NetworkError and detect_auth_method defaults to AuthMethod::Header (mod.rs:116-121). The whoami NetworkError default-to-header path is tested (network_error_defaults_to_header), but that returns at mod.rs:92 before valid_login is ever reached. The valid_login-specific NetworkError default is a distinct branch and is uncovered.",
      "why_uncovered": "network_error_defaults_to_header points the whole detection at 127.0.0.1:1, so whoami itself network-errors and short-circuits. No test makes whoami succeed-as-404 while valid_login network-errors. The probe_valid_login send-error and text-error NetworkError arms (valid_login.rs:88-91, 100-103) are likewise never hit.",
      "severity": "low",
      "suggested_test": "Mount /rest/whoami -> 404 on a wiremock server, but point valid_login at an unreachable host is hard with one base URL; instead use a wiremock that 404s whoami and resets/drops the valid_login connection (or returns a malformed chunked body) to force a reqwest send/read error, then assert detect_auth_method returns Ok(AuthMethod::Header). Alternatively unit-test probe_valid_login directly against a closed port.",
      "subsystem": "client-auth-version",
      "verdict": {
        "id": "client-auth-version-validlogin-network-error",
        "confirmed": true,
        "reason": "Genuine coverage gap. The ValidLoginOutcome::NetworkError arm at src/client/auth/mod.rs:116-122 fires only when whoami returns NotFound/AuthRejected AND the valid_login probe then network-errors. No test sets this up: `network_error_defaults_to_header` (mod_tests.rs:195-202) targets 127.0.0.1:1, so whoami itself errors and short-circuits at mod.rs:87-92 (WhoamiOutcome::NetworkError) before valid_login runs. Every test that actually reaches valid_login uses a live wiremock MockServer returning real HTTP responses \u2014 whoami_404_falls_back_to_valid_login_header (mod_tests.rs:51), valid_login_query_param_but_header_works_on_api (:80), valid_login_query_param_and_header_fails_on_api (:128), both_methods_fail_with_email (:253), valid_login_accepts_integer_result (:224) \u2014 none induce a send error or body-text-read error, so the NetworkError arms in probe_valid_login (valid_login.rs:88-91 send error, :100-103 text error) are never hit either. valid_login_tests.rs only covers ValidLoginResult/ValidLoginResponse deserialization (no network). No functional tests reference valid_login or NetworkError (grep of tests/functional/ empty). The branch is genuinely uncovered.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-auth-version-apply-auth-fallback",
      "kind": "coverage-gap",
      "title": "version probe apply_auth-failure fallback to unauthenticated request is untested",
      "location": "/Users/dave/src/bzr/src/client/version.rs:22-29",
      "description": "detect_version_and_mode calls apply_auth on the version request; if that fails (e.g. an api_key containing characters invalid for a header value when auth_method == Header), it logs and silently falls back to an unauthenticated GET (version.rs:27). No test drives apply_auth to the Err arm, so the fallback request construction is uncovered. This matters because detect_server_settings validates the key earlier via HeaderValue::from_str, but detect_version_and_mode is pub(super) and called directly in version_tests.rs with arbitrary keys \u2014 the fallback is reachable in isolation.",
      "why_uncovered": "All version_tests.rs cases pass AuthMethod::Header with a clean 'test-key', so apply_auth always succeeds. None pass an invalid key or a method whose apply_auth can fail.",
      "severity": "low",
      "suggested_test": "Add a #[tokio::test] calling detect_version_and_mode with api_key containing a newline (\"bad\\nkey\") and AuthMethod::Header against a wiremock that returns 200 {\"version\":\"5.1.2\"} for an unauthenticated GET; assert it still returns (Some(\"5.1.2\"), Rest), proving the fallback path was taken.",
      "subsystem": "client-auth-version",
      "verdict": {
        "id": "client-auth-version-apply-auth-fallback",
        "confirmed": true,
        "reason": "Confirmed uncovered. version.rs:22-29 falls back to an unauthenticated http.get(&url) when apply_auth returns Err. All five detect_version_and_mode tests in version_tests.rs (lines 39, 62, 83, 96, 117) pass a valid \"test-key\" with AuthMethod::Header, so apply_auth always returns Ok \u2014 none drives the Err arm. The Err path of apply_auth is tested only directly in http_tests.rs:89 (apply_auth_header_method_rejects_invalid_value, key \"bad\\nkey\"), which calls apply_auth standalone and never routes through detect_version_and_mode, so the fallback request construction at version.rs:27 is never executed. Functional run-tests.sh:78,129 hit /rest/version with valid keys against real servers, also not exercising the fallback. The gap is real but low severity: detect_server_settings validates the key earlier via HeaderValue::from_str, so in the real auth flow an invalid key never reaches this branch; the fallback is only reachable because detect_version_and_mode is pub(super) and tested in isolation, and its sole effect is an unauthenticated probe of the typically-public version endpoint.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-auth-version-nonnumeric-version",
      "kind": "coverage-gap",
      "title": "version_to_api_mode non-numeric / empty / single-token version strings (None arm -> Hybrid) untested",
      "location": "/Users/dave/src/bzr/src/client/version.rs:81-87",
      "description": "version_to_api_mode's final two arms are unexercised: (a) a version whose major component is non-numeric or empty (e.g. \"\", \"dev\", \"unknown\", \"v5.1\") yields major=None,minor=None and hits the `_ => ApiMode::Hybrid` catch-all (version.rs:86); (b) a version like \"5.beta\" yields (Some(5), None) and hits the (Some(5), None) => Hybrid arm. The existing tests only cover clean numeric strings ('4.4.13','5.0.4.rh103','5.1.2','6.0'). A malformed/garbage version silently defaults to Hybrid, which is a reasonable default but is an undocumented, untested behavior \u2014 and a leading 'v' prefix ('v5.1') would be misclassified as Hybrid instead of Rest.",
      "why_uncovered": "version_tests.rs version_to_mode_* cases all use well-formed numeric versions; none feed an empty string, alpha-leading token, or 'v'-prefixed string.",
      "severity": "low",
      "suggested_test": "Add assert_eq! cases to version_to_mode tests: version_to_api_mode(\"\") == Hybrid, version_to_api_mode(\"unknown\") == Hybrid, version_to_api_mode(\"5.beta\") == Hybrid, and document/decide version_to_api_mode(\"v5.1\") behavior (currently Hybrid) with an explicit assertion so a future parser change can't silently regress it.",
      "subsystem": "client-auth-version",
      "verdict": {
        "id": "client-auth-version-nonnumeric-version",
        "confirmed": true,
        "reason": "Confirmed real coverage gap. The direct version_to_api_mode unit tests in src/client/version_tests.rs (version_to_mode_pre_5 lines 8-10, version_to_mode_5_0 lines 14-17, version_to_mode_5_1_plus lines 21-24) only feed well-formed numeric strings (4.4.13, 3.6.1, 5.0, 5.0.4, 5.0.4.rh103, 5.1, 5.1.2, 6.0). None of these produce major=None or a (Some(5), None) tuple, so the final catch-all `_ => ApiMode::Hybrid` (version.rs:86) and the `(Some(5), None)` arm (version.rs:84) are never directly exercised. The detect_version_non_json test (version_tests.rs:108-126) does return Hybrid, but via the serde-parse-failure early-return at version.rs:61 (it asserts version.is_none()), which bypasses version_to_api_mode entirely \u2014 it does NOT cover line 86. A successfully-parsed VersionResponse with a garbage version (\"dev\"/\"\"/\"v5.1\") would route into version_to_api_mode and hit the untested arms. Integration tests (tests/integration.rs:372,399,2075) and functional tests (tests/functional/run-tests.sh:79) only use numeric versions or check endpoint availability. The behavior itself is a correct/safe default, so this is purely an untested-path gap, not a bug.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-misc-whoami-fallback-uncovered",
      "kind": "coverage-gap",
      "title": "whoami fallback for Bugzilla < 5.1 (error 32614 / HTTP 404) is entirely untested",
      "location": "src/client/user.rs:12-27",
      "description": "`whoami()` has a fallback branch: when `/rest/whoami` returns API error code 32614 or raw HTTP 404, it falls back to `whoami_via_user_lookup(email)` if `email_hint` is set, otherwise returns a synthetic `BzrError::Api{code:32614, ...}` with a message advising the user to add `--email`. None of the three fallback outcomes is exercised: (a) 32614 + email present \u2192 user lookup succeeds, (b) 404 + email present \u2192 user lookup, (c) 32614/404 + no email \u2192 synthetic error. The user-lookup-empty NotFound path in `whoami_via_user_lookup` (user.rs:37-40) is also untested.",
      "why_uncovered": "`whoami_returns_user_info` (user_tests.rs:10-29) only mocks a 200 success at `/rest/whoami`. The integration test `whoami_integration` (tests/integration.rs:289) and functional test 8 (run-tests.sh:131) likewise hit a server that has the endpoint. No test ever returns 32614 or 404 from `/rest/whoami`, so the entire Bugzilla 5.0 compatibility path is dead to the test suite.",
      "severity": "medium",
      "suggested_test": "Add three `#[tokio::test]` cases in user_tests.rs using wiremock: (1) `/rest/whoami` returns 200 body `{\"error\":true,\"code\":32614,\"message\":\"...\"}` plus `/rest/user?names=<email>` returning a user; build client with `email_hint: Some(...)`; assert the resolved WhoamiResponse matches the looked-up user. (2) `/rest/whoami` returns HTTP 404 (non-JSON), same user lookup, assert fallback succeeds. (3) `email_hint: None` with 32614 response; assert `Err(BzrError::Api{code:32614,..})` whose message mentions `--email`.",
      "subsystem": "client-misc",
      "verdict": {
        "id": "client-misc-whoami-fallback-uncovered",
        "confirmed": true,
        "reason": "Real coverage gap. BugzillaClient::whoami() at src/client/user.rs:12-25 has a fallback branch on BzrError::Api{code:32614} or HttpStatus{status:404} that calls whoami_via_user_lookup (user.rs:31-41) when email_hint is set, else returns a synthetic 32614 error. The only test of this method, whoami_returns_user_info (user_tests.rs:10-29), mocks a 200 success at /rest/whoami; integration test whoami_integration (tests/integration.rs:289-318) and e2e_whoami_via_cli_args (integration.rs:2039) also return 200; functional cases 8/8a/103 (run-tests.sh:131-136, 1403) hit a live server with the endpoint present. The 32614 references at run-tests.sh:197,209 are for `component update`, not whoami. I verified the apparent counter-evidence: src/client/auth/mod_tests.rs has whoami_404_falls_back_to_valid_login_header and whoami_404_no_email_suggests_email_flag, but those exercise the SEPARATE auth-detection probe detect_whoami_auth/probe_whoami in src/client/auth/whoami.rs (which falls back to valid_login), not BugzillaClient::whoami()'s fallback to whoami_via_user_lookup. None of the three fallback outcomes (32614+email, 404+email, no-email synthetic error) nor the empty-result NotFound path (user.rs:37-40) is exercised by any test. The code itself reads correctly; this is a missing-test gap, not a logic bug.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "client-misc-create-user-hybrid-xmlrpc-uncovered",
      "kind": "coverage-gap",
      "title": "create_user Hybrid REST-failure fallback and XmlRpc routing are untested",
      "location": "src/client/user.rs:55-67",
      "description": "`create_user` branches on `api_mode`: Rest posts JSON; XmlRpc routes straight to `xmlrpc_client()?.create_user`; Hybrid attempts REST and on ANY error logs and retries via XML-RPC. The Hybrid fallback-on-error branch and the XmlRpc direct-routing branch have no BugzillaClient-level test. Notably the Hybrid path falls back on *any* error (including a 403/auth error), unlike `get_group` which only falls back on 32610 or transport failures \u2014 worth a test to lock in (or reconsider) that behavior.",
      "why_uncovered": "user_tests.rs only constructs `test_client` (Rest mode). `create_user_returns_id` and `create_user_forbidden` exercise only the Rest arm. The XML-RPC `create_user` test at src/xmlrpc/client_tests.rs:472 tests the XmlRpcClient struct directly, not the BugzillaClient dispatch/fallback in user.rs. `group_tests.rs` has hybrid coverage but `user_tests.rs` imports no hybrid/xmlrpc helper.",
      "severity": "medium",
      "suggested_test": "In user_tests.rs add: (1) Hybrid client where `POST /rest/user` returns a 200 error envelope and `POST /xmlrpc.cgi` returns a User.create XML-RPC response with an id; assert the XML-RPC id is returned and REST was attempted once. (2) XmlRpc-mode client expecting 0 hits on `/rest/user` and 1 hit on `/xmlrpc.cgi`. (3) Hybrid where REST returns a 403 auth error \u2014 document whether fallback to XML-RPC is intended (it currently happens).",
      "subsystem": "client-misc",
      "verdict": {
        "id": "client-misc-create-user-hybrid-xmlrpc-uncovered",
        "confirmed": true,
        "reason": "Real gap. src/client/user.rs:55-67 dispatches create_user across Rest (line 57), XmlRpc direct routing (line 58), and Hybrid fallback-on-ANY-error (lines 59-65). src/client/user_tests.rs:7 imports only test_client (Rest mode); the two create_user tests create_user_returns_id (line 95) and create_user_forbidden (line 119) both use test_client, exercising only the ApiMode::Rest arm. The XML-RPC test at src/xmlrpc/client_tests.rs:465-472 constructs XmlRpcClient directly, not BugzillaClient, so it does not cover the dispatch/fallback in user.rs. group_tests.rs:8 imports test_client_hybrid with dedicated hybrid/xmlrpc dispatch tests (lines 243, 280, 351, 389), proving the harness exists but is not applied to create_user. Functional run-tests.sh:265-266 runs \"user create\" under the default api mode only; --api hybrid/--api xmlrpc appear only for comment/attachment list (lines 1059, 1105, 1117, 1311). So neither the Hybrid fallback-on-error branch nor the XmlRpc routing branch of create_user has any BugzillaClient-level or functional test, and the \"fall back on any error including 403/auth\" behavior is unverified.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "client-misc-list-products-multichunk-uncovered",
      "kind": "coverage-gap",
      "title": "list_products_by_type multi-chunk (>50 ids) batching is untested",
      "location": "src/client/product.rs:30-39",
      "description": "`list_products_by_type` splits accessible ids into `chunks(50)` and issues one `GET /rest/product?ids=...` per chunk, accumulating results. The chunk loop boundary (>50 ids requiring 2+ requests, and result ordering/accumulation across chunks) is never tested. A 200-error or deserialization failure in a *later* chunk would propagate, but that error path mid-iteration is also untested.",
      "why_uncovered": "`list_products_returns_products` (product_tests.rs:9-39) mocks exactly 2 ids \u2192 a single chunk/request. `list_products_empty` covers the early-return. No test supplies >50 accessible ids, so the chunking loop never iterates more than once and the 50-boundary is unverified.",
      "severity": "low",
      "suggested_test": "Add a test mounting `/rest/product_accessible` returning 51+ ids, and a `/rest/product` mock with `.expect(2)` (or matchers distinguishing chunk 1 vs chunk 2) returning disjoint product sets; assert all products are accumulated in order and exactly 2 product requests were made. Add a second test where the second chunk returns an error envelope and assert the call fails.",
      "subsystem": "client-misc",
      "verdict": {
        "id": "client-misc-list-products-multichunk-uncovered",
        "confirmed": true,
        "reason": "Confirmed gap. The chunks(50) loop at src/client/product.rs:31-38 is exercised only with single-chunk inputs in every test: src/client/product_tests.rs:14 supplies ids [1,2] (1 chunk) and :46 supplies [] (empty early-return), src/commands/product_tests.rs:16 supplies [1,2], and tests/integration.rs:329 supplies [1]. A grep across src/ and tests/ for list_products_by_type/product_accessible shows no other call sites. No test supplies >50 accessible ids, so the loop never iterates twice \u2014 the 50-element boundary (1 request vs 2+), cross-chunk accumulation/ordering via all_products.extend (line 37), and a mid-iteration error in a later chunk (self.send/parse_json propagating via ? at lines 35-36) are all unverified. Functional run-tests.sh:158 'product list' runs against a real container with only a few products, never reaching 50. Severity is low because the loop is simple stdlib chunks(50)+extend, and the empty and single-chunk happy paths plus per-request error handling are already covered.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-misc-field-empty-values-vs-notfound",
      "kind": "coverage-gap",
      "title": "get_field_values: empty-values (field exists, no legal values) vs NotFound distinction untested",
      "location": "src/client/field.rs:24-36",
      "description": "The doc comment states an empty `fields` array means NotFound, while a present field with an empty `values` array means 'field exists but has no legal values' and should return `Ok(vec![])`. Only the NotFound (empty `fields`) case is tested. The `Ok(empty vec)` case \u2014 `{\"fields\":[{\"values\":[]}]}` \u2014 is not, so the documented contract (and the `.next()` -> Ok path with an empty values list) is unverified.",
      "why_uncovered": "`get_field_values_unrecognized_field_returns_not_found` (field_tests.rs:103-124) covers `{\"fields\":[]}` \u2192 NotFound. `get_field_values_returns_values` and the severity-alias test return non-empty values. Neither asserts the empty-`values`-but-present-field branch.",
      "severity": "low",
      "suggested_test": "Add a `#[tokio::test]` mocking `/rest/field/bug/bug_status` \u2192 `{\"fields\":[{\"values\":[]}]}` and assert `get_field_values(\"status\").await.unwrap().is_empty()` (Ok, not NotFound), pinning the documented distinction.",
      "subsystem": "client-misc",
      "verdict": {
        "id": "client-misc-field-empty-values-vs-notfound",
        "confirmed": true,
        "reason": "Verified all three tests in src/client/field_tests.rs. get_field_values_returns_values (lines 56-79) and get_field_values_resolves_severity_alias (lines 81-101) both mount responses with non-empty `values` arrays. get_field_values_unrecognized_field_returns_not_found (lines 103-124) mounts `{\"fields\": []}` and asserts NotFound. No test mounts `{\"fields\":[{\"values\":[]}]}` to assert the documented Ok(vec![]) path (field exists but has no legal values), per the doc comment at field.rs:21-23 and the `Ok(field.values)` return at field.rs:35. Functional tests (run-tests.sh cases 16-19c) cover `field list` for real fields and the nonexistent NotFound case but cannot deterministically produce the empty-values branch. The gap is real. Severity low: the code path is a trivial passthrough of an empty Vec with no value-specific branching, the behavior is correct, and regression risk is minimal.",
        "refined_severity": "low"
      }
    },
    {
      "id": "client-misc-server-info-partial-failure",
      "kind": "coverage-gap",
      "title": "server_info: failure of the extensions call after version succeeds is untested",
      "location": "src/client/server.rs:7-14",
      "description": "`server_info()` makes two sequential requests (`server_version` then `server_extensions`) and `?`-propagates. The case where `/rest/version` succeeds but `/rest/extensions` fails (e.g. 500 or a 200 error envelope) \u2014 leaving the user with a half-fetched result that becomes an error \u2014 is not exercised.",
      "why_uncovered": "server_tests.rs tests `server_version` and `server_extensions` in isolation (lines 9, 25); it never calls `server_info()`. The integration `server_info_integration` (tests/integration.rs:366) mocks both endpoints returning 200, covering only the combined happy path.",
      "severity": "low",
      "suggested_test": "Add a server_tests.rs `#[tokio::test]` where `/rest/version` returns 200 with a version and `/rest/extensions` returns a 500 (or 200 error envelope); assert `server_info().await` is `Err` and surfaces the extensions failure rather than a version-shaped success.",
      "subsystem": "client-misc",
      "verdict": {
        "id": "client-misc-server-info-partial-failure",
        "confirmed": true,
        "reason": "The specific ordering claimed \u2014 /rest/version returns 200 but /rest/extensions fails \u2014 is genuinely untested. The nearest test, server_info_http_500_returns_error (src/commands/server_tests.rs:49), mocks /rest/version returning 500, so the FIRST call fails and extensions is never reached (no extensions mock is even mounted). All other server_info tests mock BOTH endpoints returning 200: server_info_integration (tests/integration.rs:366-384), e2e_server_info_via_cli_args (tests/integration.rs:2072-2087), and server_info_returns_version_and_extensions (src/commands/server_tests.rs:11-28). The sibling client server_tests.rs only tests server_version (line 9) and server_extensions (line 25) in isolation, never server_info(). grep of tests/functional/ shows no server-extensions failure coverage. So no test exercises the version-succeeds/extensions-fails branch. Severity is low, however: server_info() (src/client/server.rs:7-14) is trivial sequential ?-propagation, and server_extensions() (line 20-21) just calls the same shared get_json(\"extensions\") helper whose HTTP-error propagation is already exercised by server_info_http_500_returns_error and many other tests. No partial state is persisted on failure \u2014 the already-fetched version is simply discarded \u2014 so the failure mode is identical to the first-call path and carries no distinct logic.",
        "refined_severity": "low"
      }
    },
    {
      "id": "commands-bug-clone-comment-failure-after-create",
      "kind": "likely-bug",
      "title": "Clone reports failure (Err) even though the bug was already created when the \"Cloned from\" comment POST fails",
      "location": "/Users/dave/src/bzr/src/commands/bug/clone.rs:87-101",
      "description": "create_bug() at line 87 creates the new bug and returns its id. Then lines 89-93 issue a *separate* POST to add the \"Cloned from bug #N\" comment, and the `?` propagates any error. If the comment POST fails (transient network error, insufficient permission to comment, server hiccup), `handle()` returns Err and the success line at 95-100 never prints. The user sees a failure and a non-zero exit, but the clone bug actually exists on the server. There is no rollback and no message telling the user the new bug id. This is a non-atomic two-step operation presented as one; the failure mode silently misleads the user into thinking nothing was created (and likely retrying, producing a duplicate clone).",
      "why_uncovered": "clone_tests.rs covers only the happy path (bug_clone_copies_fields mounts a 200 for the comment POST) and bug_clone_no_comment_skips_comment (expect(0) on the comment endpoint). Neither test makes the comment POST fail after a successful create, so the misleading-Err path and the lost-bug-id behavior are completely unexercised.",
      "severity": "medium",
      "suggested_test": "Add a #[tokio::test] mounting GET /rest/bug/100 + GET /rest/bug/100/comment (description) + POST /rest/bug -> 200 {id:200}, but POST /rest/bug/200/comment -> 500. Assert the command result is Err AND assert the emitted stdout/stderr surfaces the created bug id 200 (which it currently does not), documenting the desired behavior: either succeed-with-warning, or fail-but-report-the-created-id.",
      "subsystem": "commands-bug",
      "verdict": {
        "id": "commands-bug-clone-comment-failure-after-create",
        "confirmed": true,
        "reason": "Code confirms the non-atomic flow: src/commands/bug/clone.rs:87 creates the bug (returns new_id), then lines 90-92 issue a separate add_comment POST whose error is propagated via `?`, returning Err before write_result at line 95 \u2014 so a comment-POST failure reports total clone failure while the bug already exists on the server, and new_id is never surfaced. No rollback. Tests do NOT cover this: clone_tests.rs:77-82 (bug_clone_copies_fields) mounts a 200 success for /rest/bug/200/comment; clone_tests.rs:161-166 (bug_clone_no_comment_skips_comment) uses expect(0) so the comment is never posted. Functional cases 54-57 in tests/functional/run-tests.sh:764-791 run against real Bugzilla where the POST succeeds. No test mounts a non-2xx response for the comment endpoint after a successful create with no_comment=false, so the misleading-Err / lost-bug-id path is unexercised.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "commands-bug-clone-missing-comment-zero",
      "kind": "coverage-gap",
      "title": "Clone when source bug has no comment #0 yields a description-less clone with no diagnostic",
      "location": "/Users/dave/src/bzr/src/commands/bug/clone.rs:40-45",
      "description": "When --description is not given, clone fetches comments and does `comments.into_iter().find(|c| c.count == 0).map(|c| c.text)`. If the server returns no comment with count==0 (private/redacted description, comment-list permission denied returning empty, or an unusual bug with no comment #0), clone_description becomes None and the new bug is created with description: None. The CreateBugParams description is then omitted. No error, no warning \u2014 the clone silently loses the original description. The branch where find() returns None is never asserted.",
      "why_uncovered": "Both clone tests mount a comment list that contains a count==0 comment, so the Some(text) branch is always taken. The None branch (no count==0 comment) is never exercised, so neither the silent-empty-description behavior nor any intended fallback is validated.",
      "severity": "medium",
      "suggested_test": "Add a test mounting GET /rest/bug/100/comment returning only comments with count>=1 (no count==0), POST /rest/bug succeeding. Assert the POST body has no `description` key (or assert a warning is emitted), pinning down the intended behavior when the source description can't be recovered.",
      "subsystem": "commands-bug",
      "verdict": {
        "id": "commands-bug-clone-missing-comment-zero",
        "confirmed": true,
        "reason": "Code at src/commands/bug/clone.rs:44 does `comments.into_iter().find(|c| c.count == 0).map(|c| c.text)`; the result feeds `description: clone_description` (line 71) with no warning or error if None. Grep confirms this is the only count==0 site \u2014 no fallback exists. Both unit tests mount a count==0 comment: bug_clone_copies_fields (clone_tests.rs:52-58) and bug_clone_no_comment_skips_comment (clone_tests.rs:140-148), so the Some(text) branch is always taken. Functional cases 54-57 (run-tests.sh:764-791) all clone BUG3, which is created with `--description \"Description for cloning\"` (run-tests.sh:594), guaranteeing a comment #0. No test exercises a source bug lacking comment #0, so the silent description-loss None branch is never asserted.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "commands-bug-my-created-and-cc-combo",
      "kind": "coverage-gap",
      "title": "`bug my --created --cc` (both set, --all absent) is a valid, untested flag combination that skips the assigned search",
      "location": "/Users/dave/src/bzr/src/commands/bug/my.rs:46-60",
      "description": "In the CLI (src/cli/bug.rs:447-464) only --all is mutually exclusive with --created/--cc; --created and --cc are NOT mutually exclusive with each other. So `bug my --created --cc` parses fine. In my.rs the assigned search runs only `if *all || (!created && !cc)` (line 46); with both created and cc true that condition is false, so the assigned-to search is skipped and only the creator (line 51) and cc (line 56) searches run. This is a real reachable combination whose semantics (assigned bugs excluded) are surprising and untested.",
      "why_uncovered": "my_tests.rs covers default (assigned), --created alone, --cc alone, and --all. The created+cc-together permutation is never constructed, so the branch selection (assigned skipped, creator+cc run) and dedup across exactly those two searches are unverified.",
      "severity": "low",
      "suggested_test": "Add a #[tokio::test] with BugAction::My { created:true, cc:true, all:false, .. }, mount whoami plus GET /rest/bug matchers for creator=<me> and cc=<me>, and a NOT-matcher / expect(0) ensuring no request carries assigned_to=<me>. Assert results merge/dedup from the two searches only.",
      "subsystem": "commands-bug",
      "verdict": {
        "id": "commands-bug-my-created-and-cc-combo",
        "confirmed": true,
        "reason": "Verified the code: src/cli/bug.rs:463 puts conflicts_with_all=[\"created\",\"cc\"] only on `all`; `created` (line 451) and `cc` (line 457) have no mutual conflict, so `bug my --created --cc` parses fine. In src/commands/bug/my.rs:46 the assigned search guard `*all || (!created && !cc)` evaluates false when both created and cc are true, so the assigned-to search is skipped while creator (line 51) and cc (line 56) searches run. The sibling tests src/commands/bug/my_tests.rs cover only: default/assigned (line 22), assigned+filters (line 66), --created alone (line 105, asserts NOT assigned/cc), --cc alone (line 142), and --all dedup (line 178). No test constructs `created:true, cc:true, all:false`. Functional tests in tests/functional/run-tests.sh cases 61 (assigned), 62 (--created), 63 (--all), 64/64a-c (--all --status) likewise never combine --created with --cc. So the branch selection (assigned skipped; creator+cc only) and the two-search dedup for this permutation are genuinely untested and reachable.",
        "refined_severity": "low"
      }
    },
    {
      "id": "commands-bug-update-batch-empty-comment-suffix-on-failure",
      "kind": "coverage-gap",
      "title": "Batch update table output: '(with comment)' suffix prints even when the only succeeding bugs got the comment but some failed, and the all-failed case is untested",
      "location": "/Users/dave/src/bzr/src/commands/bug/update.rs:217-239",
      "description": "write_batch_result prints 'Updated bugs: ...(with comment)' guarded only by `!batch.succeeded.is_empty()` and `with_comment`. When every bug in a batch fails, succeeded is empty so the header is skipped and only per-failure stderr lines print \u2014 but this all-failed batch path (table format, with a comment requested) is never tested. ensure_batch_complete still returns BatchPartialFailure{succeeded:0, failed:N}. The interaction of an all-failed batch with the comment suffix and the BatchPartialFailure exit code in table mode is unverified.",
      "why_uncovered": "update_tests.rs covers mixed (1 ok / 1 fail) in JSON and all-succeed in table, plus single-bug comment-suffix cases. No test drives a batch where every PUT fails in table mode, so the empty-succeeded header-skip branch and the with_comment suffix suppression in that case are unexercised.",
      "severity": "low",
      "suggested_test": "Add a #[tokio::test] with ids vec![1,2], both PUT /rest/bug/{id} -> 500, comment Some(\"x\"), OutputFormat::Table. Assert result is Err(BatchPartialFailure{succeeded:0,failed:2}), that stdout contains no 'Updated bugs:' line, and that stderr lists both failures.",
      "subsystem": "commands-bug",
      "verdict": {
        "id": "commands-bug-update-batch-empty-comment-suffix-on-failure",
        "confirmed": true,
        "reason": "The coverage gap is real (though no actual defect exists \u2014 the code behaves correctly). Tests in update_tests.rs cover: mixed succeeded:1/failed:1 in JSON only (bug_update_batch_mixed_results, line 256-291); all-succeed table (bug_update_batch_table_format_all_succeed, line 294-316); and all-succeed table WITH comment suffix (bug_update_table_output_with_comment_batch_all_succeed, line 680-699). Grep for \"succeeded: 0\" returns nothing and the only 500-mock is in the mixed test where bug #1 still succeeds. The functional suite case 58 (run-tests.sh:802) is also all-succeed. So no test drives a batch where every PUT fails (succeeded:0, failed:N) in table mode with a comment requested \u2014 the empty-succeeded header-skip branch at update.rs:228 combined with the with_comment suffix suppression and BatchPartialFailure exit code in table mode is unexercised. NOTE: the claim's title implication that the suffix wrongly prints when some bugs fail is NOT a bug \u2014 at update.rs:228-232 the suffix is gated by `!batch.succeeded.is_empty() && with_comment`, so it correctly applies only to the bugs that genuinely received the comment, and the all-failed case correctly skips the header entirely while ensure_batch_complete (line 241-247) still returns BatchPartialFailure. The behavior is correct; only the test is missing.",
        "refined_severity": "low"
      }
    },
    {
      "id": "commands-misc-config-mutual-exclusion-uncovered",
      "kind": "coverage-gap",
      "title": "config set-server: neither/both --api-key and --api-key-env validation untested",
      "location": "src/commands/config.rs:158",
      "description": "set_server enforces `api_key.is_some() == api_key_env.is_some()` returning an InputValidation error 'provide exactly one of --api-key or --api-key-env'. This guards both the both-supplied and neither-supplied cases. No unit test exercises either branch.",
      "why_uncovered": "config_tests.rs covers set_server with --api-key (lines 92+) and with --api-key-env (line 320) individually, but never passes both or neither. The error message and exit code (InputValidation -> exit 7) for this guard are unverified, so a regression that drops the guard or flips the equality would pass CI.",
      "severity": "medium",
      "suggested_test": "Two #[tokio::test] cases calling config::execute with ConfigAction::SetServer where (a) both api_key and api_key_env are Some, and (b) both are None; assert result.is_err(), err.exit_code()==7, and the message contains 'exactly one of'.",
      "subsystem": "commands-misc",
      "verdict": {
        "id": "commands-misc-config-mutual-exclusion-uncovered",
        "confirmed": true,
        "reason": "Guard at src/commands/config.rs:158 (`api_key.is_some() == api_key_env.is_some()` -> InputValidation \"provide exactly one of --api-key or --api-key-env\") is genuinely untested for both branches. All SetServer test cases in src/commands/config_tests.rs exercise only the valid single-credential path: api_key=Some/env=None at lines 25/99/137/159/201/277, and api_key=None/env=Some at lines 327/431. None pass both Some (true==true) nor both None (false==false). The only InputValidation assertion in the file (config_tests.rs:544) is for MigrateToKeyring without --yes, a different code path. Functional tests (tests/functional/run-tests.sh:93,101,105) only use --api-key. The error string appears nowhere except its definition site \u2014 no test asserts on it. A regression dropping the guard or flipping == to != would pass CI.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "commands-misc-config-tls-pin-clear-uncovered",
      "kind": "coverage-gap",
      "title": "config set-server --tls-pin-clear: both success and not-found branches untested",
      "location": "src/commands/config.rs:142-156",
      "description": "The --tls-pin-clear branch clears tls_pin_sha256/issuer/issuer_der on an existing server (success: writes stderr 'Certificate pin cleared' and returns early) or errors with 'server ... not found \u2014 nothing to clear' when the server is absent. Neither path is covered.",
      "why_uncovered": "config_tests.rs always passes tls_pin_clear: false (all 14 occurrences). The early-return clearing logic, the config.save() of the cleared fields, and the not-found error are entirely unexercised; a bug that cleared the wrong fields or skipped save() would go undetected.",
      "severity": "medium",
      "suggested_test": "Seed a server with tls_pin_sha256/tls_pin_issuer set, run set-server with tls_pin_clear:true, reload config and assert all three pin fields are None. Second test: run tls_pin_clear:true against a nonexistent server name and assert the 'nothing to clear' config error.",
      "subsystem": "commands-misc",
      "verdict": {
        "id": "commands-misc-config-tls-pin-clear-uncovered",
        "confirmed": true,
        "reason": "Verified genuinely uncovered. In src/commands/config_tests.rs all 8 SetServer invocations pass tls_pin_clear: false (lines 33, 107, 144, 167, 209, 285, 335, 439), and tests/integration.rs:1876 also passes false \u2014 so execute()/set_server() never enters the early-return clear branch at src/commands/config.rs:142-156. The only tests referencing --tls-pin-clear, in src/cli/mod_tests.rs (parse_set_server_tls_pin_clear at line 1327 and parse_set_server_tls_pin_clear_conflicts_with_tls_pin_now at line 1391), only exercise clap argument parsing and the clap conflict with --tls-pin-now; they do not call set_server, so the field-clearing, config.save(), 'Certificate pin cleared' stderr (lines 146-151), and the 'server ... not found \u2014 nothing to clear' error (lines 153-155) are entirely unexercised. No functional shell test covers it either. A regression clearing the wrong fields or skipping save() would go undetected.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "commands-misc-attachment-download-path-traversal",
      "kind": "likely-bug",
      "title": "attachment download writes to unsanitized server-supplied file_name",
      "location": "src/commands/attachment.rs:255-257",
      "description": "download_single computes `dest = out.unwrap_or(&filename)` where filename comes straight from the server (client.download_attachment returns attachment.file_name at src/client/attachment.rs:138) and calls std::fs::write(dest, &data) with no sanitization. A server returning a file_name like '../../etc/cron.d/x' or an absolute path causes a write outside the current directory. The batch path write_one_attachment (line 295) joins '{att.id}.{att.file_name}' which still allows '/' in file_name to traverse out of the bug subdir.",
      "why_uncovered": "attachment_tests.rs exercises download with benign file names (e.g. 'a.txt') and only verifies the att_id prefix / collision resolution; no test feeds a file_name containing path separators or '..'. The traversal path is therefore unprotected and unobserved.",
      "severity": "medium",
      "suggested_test": "Mock get_attachment / bug attachments to return file_name='../escape.txt' (and an absolute path variant). Run download_single and download_batch; assert the written file stays within the intended out/out_dir (basename only) or that the command rejects the malicious name. The test will currently demonstrate the file landing outside the target dir.",
      "subsystem": "commands-misc",
      "verdict": {
        "id": "commands-misc-attachment-download-path-traversal",
        "confirmed": true,
        "reason": "Code confirmed: download_single (src/commands/attachment.rs:255-257) writes std::fs::write(out.unwrap_or(&filename), &data) where filename is the raw server-supplied attachment.file_name returned by client.download_attachment (src/client/attachment.rs:138) with zero sanitization; write_one_attachment (src/commands/attachment.rs:295) joins format!(\"{}.{}\", att.id, att.file_name) so a file_name containing embedded separators like \"../../x\" still escapes the bug subdir via Path::join. The only file_name() basename extraction (attachment.rs:66) is on the upload path, not download. Test gap confirmed: every fixture in src/commands/attachment_tests.rs uses benign names \u2014 \"patch.diff\", \"trace.log\", \"extra.bin\", \"old.patch\" (lines 852-853, 967-968, 1005, 1015, 1238, 1280, 1296) and integration line 22. The collision test attachment_download_batch_collision_filenames_resolved_by_att_id_prefix (line 958) only checks the att-id prefix disambiguates two \"trace.log\" files; no test feeds a filename with '/', '\\\\', or '..'. Functional run-tests.sh cases 98/100i/100j download benign server names or use explicit --out to /tmp. The traversal path is genuinely unprotected and unobserved.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "commands-misc-flags-name-contains-status-char",
      "kind": "likely-bug",
      "title": "parse_single_flag mis-splits flag names containing +, -, ?, or X",
      "location": "src/commands/flags.rs:24-37",
      "description": "parse_single_flag uses s.find(['+','-','?','X']) to locate the status character, taking the FIRST occurrence. If a flag type name itself begins with or contains one of these characters (notably uppercase 'X', which is a legal character in Bugzilla flag names), the split point is wrong: e.g. 'Xfeature+' yields name='' -> 'flag name cannot be empty', and 'feaXture+' yields name='fea', status=Clear, with the trailing 'ture+' treated as a malformed requestee. The intended status char is the trailing one, not the first.",
      "why_uncovered": "flags_tests.rs only tests names without any of these characters ('review', 'approval'). No test covers a flag name containing 'X' or a status symbol, so the first-match assumption is untested. Severity is low because such flag names are uncommon, but the parser is demonstrably wrong for them.",
      "severity": "low",
      "suggested_test": "Add parse_flags(&[\"Xfeature+\"]) and parse_flags(&[\"feaXture?\"]) cases asserting the parsed name and FlagStatus match the trailing-status interpretation (name='Xfeature', status=Grant). Currently both produce wrong results, confirming the bug; fix by scanning for the status char from the end or only at the final position.",
      "subsystem": "commands-misc",
      "verdict": {
        "id": "commands-misc-flags-name-contains-status-char",
        "confirmed": true,
        "reason": "Code at src/commands/flags.rs:26 uses s.find(['+','-','?','X']) (first-match) to find the status char, but per the doc comment (lines 7-9) the grammar is name[+-?X](requestee) \u2014 the status char is the TRAILING one. 'X' is a literal uppercase letter that is legal inside a Bugzilla flag type name. For a name containing X this mis-splits: 'Xfeature+' -> status_pos=0 -> name=\"\" -> spurious \"flag name cannot be empty\" (line 34); 'feaXture+' -> matches interior X -> name=\"fea\", status=Clear, remainder=\"ture+\" -> spurious \"requestee must be in parentheses\" (line 53). The intended trailing '+' is never reached. flags_tests.rs only uses names 'review'/'approval'; parse_flag_clear (flags_tests.rs:59) tests 'reviewX' where X is correctly the trailing char, NOT an X embedded in the name, so the first-match assumption is untested. tests/functional/ has zero references to flags (rg found none). The +/-/? variants are not valid name chars in Bugzilla, but the X case is a demonstrable, uncovered defect. Low severity because flag names containing uppercase X are uncommon.",
        "refined_severity": "low"
      }
    },
    {
      "id": "commands-misc-comment-add-stdin-pipe-untested",
      "kind": "coverage-gap",
      "title": "comment add stdin/editor body-resolution path untested",
      "location": "src/commands/comment.rs:37-104",
      "description": "comment add with body:None routes to read_comment_body(), which reads piped stdin or launches $EDITOR and runs filter_comment_body. Only the inline --body path and filter_comment_body() in isolation are tested. The integration of read_comment_body (non-terminal stdin branch, the empty-after-filter rejection when an editor returns only comment lines) is not exercised end-to-end.",
      "why_uncovered": "comment_tests.rs tests comment_add_with_body (explicit body) and comment_add_empty_body_is_rejected (explicit whitespace body), plus filter_comment_body unit cases. It never drives the body:None path. The branch where stdin yields only HTML-comment lines (filter -> empty -> 'empty comment' rejection) and the non-terminal read path are uncovered.",
      "severity": "low",
      "suggested_test": "Factor body resolution so read_comment_body can be tested, or add a test that constructs CommentAction::Add{body:None} with a fake non-terminal stdin returning only '<!-- ... -->' lines and assert the InputValidation 'empty comment' error fires after filtering.",
      "subsystem": "commands-misc",
      "verdict": {
        "id": "commands-misc-comment-add-stdin-pipe-untested",
        "confirmed": true,
        "reason": "Real coverage gap. src/commands/comment.rs:37-40 routes body:None to read_comment_body() (lines 95-104), whose non-terminal stdin branch (97-100) and editor+filter branch (102-103) are never driven. comment_tests.rs only tests body:Some(...): comment_add_with_body (line 57, body=Some), comment_add_empty_body_is_rejected (line 84, body=Some(\"   \")), comment_add_api_error_returns_error (line 159, body=Some). filter_comment_body_* (lines 110-125) test the filter in isolation, not integrated via read_comment_body, so the stdin->filter->empty->'empty comment' rejection integration is uncovered. Functional cases 88/89/94a (run-tests.sh:985,994,1097) all pass --body; the only stdin/editor functional coverage (48c-48f, lines 630-681) is for bug create, not comment add.",
        "refined_severity": "low"
      }
    },
    {
      "id": "config-apimode-xmlrpc-serde-roundtrip",
      "kind": "coverage-gap",
      "title": "ApiMode::XmlRpc / AuthMethod::QueryParam persistence (save->reload) never asserted",
      "location": "src/types/common.rs:41 (#[serde(rename = \"xmlrpc\")]) and src/config.rs:37-38 (api_mode/auth_method serde)",
      "description": "ApiMode derives `#[serde(rename_all = \"snake_case\")]`, which would serialize XmlRpc to `xml_rpc`; the explicit `#[serde(rename = \"xmlrpc\")]` on line 41 overrides that to `xmlrpc`. This rename is load-bearing: detect logic, version.rs, and the on-disk TOML must all agree on the string `xmlrpc`. No test ever saves a ServerConfig with api_mode = XmlRpc (or auth_method = QueryParam) and reloads it to confirm the persisted string matches what Config::load() parses. If the rename attribute were dropped or changed, save would write `xml_rpc` and load would fail to round-trip, silently.",
      "why_uncovered": "shared_tests::uncached_auth_detects_and_persists asserts save->reload only for AuthMethod::Header / ApiMode::Rest / server_version. config_tests only covers ApiMode/AuthMethod via FromStr and Display, not serde TOML serialization. shared_tests::write_config injects `api_mode = \"rest\"` / `auth_method = \"header\"` (deserialize side only, and only the snake-case-trivial variants).",
      "severity": "low",
      "suggested_test": "Add a config_tests case that constructs a ServerConfig with api_mode=Some(ApiMode::XmlRpc), auth_method=Some(AuthMethod::QueryParam), calls Config::save(), reads the raw config.toml string and asserts it contains `api_mode = \"xmlrpc\"` and `auth_method = \"query_param\"`, then Config::load() and asserts both fields deserialize back to the same enum variants.",
      "subsystem": "config",
      "verdict": {
        "id": "config-apimode-xmlrpc-serde-roundtrip",
        "confirmed": true,
        "reason": "The load-bearing claim \u2014 that the serde TOML round-trip of ApiMode::XmlRpc (the explicit #[serde(rename = \"xmlrpc\")] at src/types/common.rs:41) is never asserted by save->reload \u2014 holds up. Evidence: (1) Unit tests only exercise the non-serde paths: src/config_tests.rs:155-158 tests ApiMode::from_str and :185-188 tests Display; neither uses serde. The only save/load roundtrip (src/config_tests.rs:56-63) uses make_server_config which sets api_mode: None / auth_method: None (lines 16-17). No test in src/ contains api_mode = \"xmlrpc\" in a TOML blob or constructs api_mode: Some(XmlRpc) for serialization (grep returned none). src/commands/shared_tests.rs:111 uncached_auth_detects_and_persists is the only save->reload-via-disk test and asserts only AuthMethod::Header / ApiMode::Rest / version (lines 154-165). connect_client_api_override_applies (shared_tests.rs:80-92) passes Some(ApiMode::XmlRpc) but only asserts result.is_ok() and never reloads \u2014 and crucially the --api override is applied only to the runtime client (shared.rs:239,393 api_override.unwrap_or(...)), never persisted; persist_detected_settings (shared.rs:12-23) writes the *detected* mode only. (2) Functional tests cannot reach it either: XmlRpc is only detected for Bugzilla major < 5 (src/client/version.rs:82), but the functional matrix is bz50/bz52/bz53 (run-all-versions.sh:6) which detect as Hybrid/Rest, so api_mode = \"xmlrpc\" is never written to disk. So if the rename were dropped/changed, serde would emit/expect xml_rpc while FromStr/Display keep \"xmlrpc\", a silent split that no test catches. (Note: the AuthMethod::QueryParam half of the claim is actually covered by functional run-tests.sh:93 set-server --auth-method query_param followed by config show at line 96, which save->reload it; but that variant is trivial snake_case with no explicit rename. The XmlRpc rename \u2014 the genuinely load-bearing case \u2014 is uncovered.)",
        "refined_severity": "low"
      }
    },
    {
      "id": "config-set-server-tls-pin-clear-untested",
      "kind": "coverage-gap",
      "title": "set-server --tls-pin-clear branch is entirely untested (both success and not-found)",
      "location": "src/commands/config.rs:143-156",
      "description": "The `--tls-pin-clear` early-return branch loads config, clears tls_pin_sha256/tls_pin_issuer/tls_pin_issuer_der on an existing server, saves, and writes a confirmation to w.err; if the server does not exist it returns a config error ('nothing to clear'). Neither path is exercised by any unit or functional test (every config_tests SetServerArgs sets tls_pin_clear: false). A regression that clears the wrong fields, fails to persist, or inverts the not-found check would go undetected. Note also this branch ignores `format` entirely (no JSON output even when --output json), unlike every other config mutation which emits a ConfigResult.",
      "why_uncovered": "All set-server tests in src/commands/config_tests.rs hard-code tls_pin_clear: false; grep confirms no test drives the clear path.",
      "severity": "medium",
      "suggested_test": "Two tests: (1) seed a server with tls_pin_sha256/issuer/issuer_der set, run set-server with tls_pin_clear=true, reload config and assert all three pin fields are None and other fields preserved; (2) run tls_pin_clear=true against a nonexistent server name and assert Err containing 'nothing to clear'.",
      "subsystem": "config",
      "verdict": {
        "id": "config-set-server-tls-pin-clear-untested",
        "confirmed": true,
        "reason": "Confirmed gap. The execution branch at src/commands/config.rs:143-156 (clear fields, save, success message, and the not-found 'nothing to clear' error) is never exercised. All 8 set_server() invocations in src/commands/config_tests.rs (lines 33, 107, 145, 167, 209, 285, 335, 439) hard-code tls_pin_clear: false, as does tests/integration.rs:1876. The only --tls-pin-clear references are clap-parsing tests parse_set_server_tls_pin_clear (src/cli/mod_tests.rs:1327) and parse_set_server_tls_pin_clear_conflicts_with_tls_pin_now (mod_tests.rs:1391) \u2014 both only assert argument parsing/conflict and never call set_server(), so neither the success path nor the not-found error path is covered. No functional test (run-tests.sh, keyring-test.sh) references the flag. The claim's secondary note is also correct: the branch returns Ok(()) at line 151 after a stderr writeln and ignores `format`, so --output json emits no ConfigResult, unlike other config mutations.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "config-resolve-api-key-env-empty-and-unset",
      "kind": "coverage-gap",
      "title": "resolve_api_key env-var error paths (unset and empty) are untested",
      "location": "src/config.rs:175-187",
      "description": "For EnvVar credential sources, resolve_api_key returns a config error when the named env var is not set, and a distinct error when it is set but empty. Both error branches are unreachable in tests; only the happy path (BZR_TEST_API_KEY set to a non-empty value) is covered. An empty-string env var is a realistic operator mistake (e.g. `export BZR_KEY=` in a profile) and currently the only thing standing between that and a confusing downstream 401 is this branch.",
      "why_uncovered": "config_tests::env_backed_server_resolves_api_key_from_environment sets the var to 'secret-from-env' and asserts success. No test sets it empty or leaves it unset before calling resolve_api_key.",
      "severity": "low",
      "suggested_test": "Under ENV_LOCK: (1) remove_var the env var, call resolve_api_key, assert Err message contains 'is not set'; (2) set_var to \"\" (empty), call resolve_api_key, assert Err message contains 'is empty'.",
      "subsystem": "config",
      "verdict": {
        "id": "config-resolve-api-key-env-empty-and-unset",
        "confirmed": true,
        "reason": "The two EnvVar error branches in resolve_api_key (src/config.rs:176-180 \"is not set\" via env::var().map_err, and src/config.rs:181-185 \"is empty\") are genuinely untested. The only test touching the EnvVar source is config_tests.rs:263 env_backed_server_resolves_api_key_from_environment, which sets BZR_TEST_API_KEY to \"secret-from-env\" and asserts resolve_api_key succeeds (config_tests.rs:284) \u2014 the happy path only. No test sets the var empty or unsets it before calling resolve_api_key. The keyring resolve tests (config_tests.rs:466/500/538) cover a different CredentialSource variant. tests/integration.rs and tests/functional/run-tests.sh contain no references to resolve_api_key, api_key_env, or the \"is not set\"/\"is empty\" messages. The code itself is correct (fails fast with distinct, actionable errors), so this is purely a missing-test gap, not a behavioral bug \u2014 hence low severity.",
        "refined_severity": "low"
      }
    },
    {
      "id": "config-unset-keyring-skips-permission-hardening",
      "kind": "likely-bug",
      "title": "unset-keyring writes config via save_config_without_validation, which never applies 0o600/0o700 hardening",
      "location": "src/commands/config.rs:410-425 (save_config_without_validation) called from src/commands/config.rs:296",
      "description": "save_config_without_validation re-implements the write but omits the private-permission logic that Config::save() applies (write_private_file with mode 0o600 on Unix, set_private_directory_permissions 0o700 on first create, and warn_on_insecure_permissions). It uses plain fs::OpenOptions without .mode(0o600). If unset-keyring is the operation that first materializes the config dir/file (or after the file was recreated), the file is written with the process umask (typically 0o644) rather than owner-only. While the post-unset file has no inline secret, the directory and file silently lose the privacy guarantee the rest of the module enforces, and warn_on_insecure_permissions is never invoked here so the user gets no warning. This diverges from the security invariant the codebase otherwise upholds.",
      "why_uncovered": "unset_keyring_removes_secret_and_clears_config asserts the keyring entry is gone and the credential fields are cleared, but never checks file permissions after unset. config_save_hardens_permissions_for_new_paths covers Config::save() only, not the unset path's separate writer.",
      "severity": "low",
      "suggested_test": "On Unix: seed a keyring-backed server, run unset-keyring such that save_config_without_validation writes the file, then assert fs::metadata(config_path).permissions().mode() & 0o077 == 0 (and that no broader-than-owner bits are set). Better fix: have unset-keyring reuse the hardened writer or set perms explicitly.",
      "subsystem": "config",
      "verdict": {
        "id": "config-unset-keyring-skips-permission-hardening",
        "confirmed": true,
        "reason": "Code divergence is real and untested. src/commands/config.rs:410-425 save_config_without_validation uses plain fs::OpenOptions().create(true).truncate(true).write(true).open() with NO .mode(0o600), and does not call set_private_directory_permissions(0o700) on first dir-create nor warn_on_insecure_permissions \u2014 all three of which Config::save() applies (src/config.rs:255-272 via write_private_file mode 0o600, set_private_directory_permissions 0o700, warn_on_insecure_permissions). Tests do not cover this path: config_save_hardens_permissions_for_new_paths (src/config_tests.rs:666-681) asserts 0o700/0o600 only after calling config.save() directly, not the unset writer; unset_keyring_removes_secret_and_clears_config (src/commands/config_tests.rs:550-585) only asserts api_key/api_key_keyring/api_key_env are None and the keychain entry is gone \u2014 it never reads file/dir mode. The functional keyring-test.sh exercises `config n` (unset) but has zero stat/chmod/permission checks. Severity is low, not high: unset-keyring requires a pre-existing keyring-backed server, so config.toml already exists at 0o600 (created by the earlier set-server/set-keyring through Config::save), and on Unix open()+truncate on an existing file does not reset its mode \u2014 so the normal flow keeps 0o600 and the dir keeps 0o700. The post-unset file also contains no secret. Insecure perms only materialize in the edge case where the dir/file was deleted/recreated between credentialing and unset, so this is a genuine but low-exposure defense-in-depth/invariant-consistency gap plus a silently-skipped insecure-permissions warning.",
        "refined_severity": "low"
      }
    },
    {
      "id": "config-corrupt-toml-error-mapping",
      "kind": "coverage-gap",
      "title": "Config::load on syntactically corrupt TOML (TomlParse error path) is untested",
      "location": "src/config.rs:246 (toml::from_str) and src/config.rs:251 (generic IO error propagation)",
      "description": "Config::load handles three load failure shapes: NotFound -> default (tested), generic IO error -> propagate (tested via path-is-a-directory), and malformed TOML content -> toml::from_str returns Err mapped to BzrError::TomlParse (NOT tested). A corrupt/truncated config file is a common real-world failure (partial write, manual edit, merge conflict markers) and the error type/exit code for it is never asserted. The index claims 'missing/corrupt config' coverage, but only missing and IO-error are actually covered; the parse-error branch has no test.",
      "why_uncovered": "config_file_io_operations covers case 1 (NotFound->default) and case 4 (directory at path->IO error). config_load_rejects_multiple_api_key_sources covers post-parse semantic validation, not a TOML syntax error. No test writes invalid TOML (e.g. `url = ` or a stray bracket) and asserts BzrError::TomlParse / its exit_code.",
      "severity": "low",
      "suggested_test": "Write `config.toml` with invalid TOML (e.g. `[servers.test]\\nurl =`), set XDG_CONFIG_HOME, call Config::load(), assert Err and matches!(err, BzrError::TomlParse(_)) with the expected exit_code().",
      "subsystem": "config",
      "verdict": {
        "id": "config-corrupt-toml-error-mapping",
        "confirmed": true,
        "reason": "Verified the gap is real. Config::load (src/config.rs:246) maps malformed TOML to BzrError::TomlParse via the `?`/`#[from]` conversion (error.rs:19). No test exercises this through Config::load: config_file_io_operations (config_tests.rs:45-84) covers only case 1 (NotFound->default, line 52) and case 4 (path-is-a-directory->IO error, line 83); config_load_rejects_multiple_api_key_sources (config_tests.rs:228-260) writes *valid* TOML and asserts a post-parse *semantic* validation error (\"multiple API key sources\"), not a syntax error. The keyring_ref_toml_roundtrip tests (lines 334-352) call toml::from_str directly on valid input, not Config::load. The only TomlParse tests, exit_code_toml_parse (error_tests.rs:36) and error_type_toml_parse (error_tests.rs:81), construct the variant directly via toml::from_str(\"{{bad\") and assert exit_code==3 / error_type==\"config\" \u2014 they never go through Config::load, so the load integration branch (a real malformed config file producing TomlParse) is untested. Severity is low: the variant's exit_code/error_type behavior IS covered in error_tests.rs and the load-site code is a one-line derived From conversion, so the only untested thing is the glue that propagates rather than swallows a parse error.",
        "refined_severity": "low"
      }
    },
    {
      "id": "config-xdg-relative-path-fallback",
      "kind": "coverage-gap",
      "title": "Config::path() relative-XDG_CONFIG_HOME fallback to dirs::config_dir is untested",
      "location": "src/config.rs:233-238",
      "description": "Config::path() deliberately filters XDG_CONFIG_HOME through `.filter(|p| p.is_absolute())` before falling back to dirs::config_dir(). This guards against the XDG spec requirement that the value be ignored if not absolute. The filter branch (relative value -> ignored -> fall back) is never tested; every test sets XDG_CONFIG_HOME to an absolute tempdir. A regression dropping the is_absolute filter would let a relative XDG value silently redirect the config path (and on a multi-server setup, write secrets to an unexpected relative location).",
      "why_uncovered": "All config tests set XDG_CONFIG_HOME to tmp.path() which is absolute, so the is_absolute filter always passes and the fallback arm is never exercised.",
      "severity": "low",
      "suggested_test": "Under ENV_LOCK with HOME/dirs mockable: set XDG_CONFIG_HOME to a relative path like \"relative/dir\", call Config::path(), and assert the returned path does NOT contain that relative segment (i.e. it fell back to dirs::config_dir), or assert it equals dirs::config_dir().join(\"bzr/config.toml\").",
      "subsystem": "config",
      "verdict": {
        "id": "config-xdg-relative-path-fallback",
        "confirmed": true,
        "reason": "Genuine coverage gap. Config::path() at src/config.rs:233-238 filters XDG_CONFIG_HOME through `.filter(|p| p.is_absolute())` before falling back to dirs::config_dir(). Every test sets the var to an absolute path: src/config_tests.rs:49,196,255,670 all use tmp.path() (tempfile::tempdir, absolute); tests/integration.rs:662,1863,1905 use tmp.path(); tests/functional/run-tests.sh:41 sets it to FUNC_CONFIG_DIR = mktemp -d /tmp/bzr-func-config.XXXXXX (line 40, absolute). No test sets a relative XDG_CONFIG_HOME value and none removes the var, so the is_absolute filter predicate always passes and the rejection/fallback arm is never executed. The code itself is correct (XDG-spec compliant); only test coverage of the filter branch is missing.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-xmlrpc-mapping-untested",
      "kind": "coverage-gap",
      "title": "XmlRpc variant exit_code()==4 / error_type()==\"api\" never asserted",
      "location": "/Users/dave/src/bzr/src/error.rs:172,192",
      "description": "BzrError::XmlRpc maps to EXIT_CODE_API (4) and ERROR_TYPE_API (\"api\") \u2014 it is grouped with Api in both match arms. No test asserts this mapping. The only place XmlRpc appears in error_tests.rs is line 255, inside is_bug_get_per_resource_false_for_transport_and_auth, which only checks is_bug_get_per_resource()==false. A mutation that moved XmlRpc to a different exit-code/error-type arm (e.g. swapping it into the HTTP or Other arm) would not be caught. XML-RPC failures are a primary error class for this CLI (hybrid/xmlrpc modes), so the user-facing exit code 4 for a protocol-level XML-RPC fault is unverified.",
      "why_uncovered": "error_tests.rs exhaustively tests Config, Api, Io, Other, TomlParse, NotFound, HttpStatus, InputValidation, Deserialize, Auth, DataIntegrity, Keyring, PinMismatch, IssuerChanged for exit_code/error_type, but skips XmlRpc, TomlSerialize, BatchPartialFailure, and bare Http. XmlRpc is only used in an is_bug_get_per_resource assertion.",
      "severity": "low",
      "suggested_test": "Add a unit test in error_tests.rs: let err = BzrError::XmlRpc(\"fault 32000\".into()); assert_eq!(err.exit_code(), 4); assert_eq!(err.error_type(), \"api\");",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-xmlrpc-mapping-untested",
        "confirmed": true,
        "reason": "Confirmed gap. error.rs:172 maps BzrError::XmlRpc to EXIT_CODE_API (4) and error.rs:192 maps it to ERROR_TYPE_API (\"api\"), grouped with Api. No test asserts this. error_tests.rs contains exit_code/error_type assertions for Config, Api, Io, Other, TomlParse, NotFound, HttpStatus, InputValidation, Deserialize, Auth, DataIntegrity, Keyring, PinMismatch, IssuerChanged \u2014 but none construct BzrError::XmlRpc and call exit_code()/error_type(). The sole XmlRpc occurrence is error_tests.rs:255 inside is_bug_get_per_resource_false_for_transport_and_auth, which only asserts is_bug_get_per_resource()==false (the arm at error.rs:151), not the exit-code/error-type mapping. Functional tests (run-tests.sh 94c/100c/100e) exercise only successful XML-RPC command paths, never an exit code 4 for an XML-RPC fault. A mutation moving XmlRpc(_) out of the API arm at line 172/192 into e.g. the HTTP arm would survive the test suite. Severity is low rather than higher because the current mapping is correct and XmlRpc aliases the same exit code (4) / type (\"api\") as the thoroughly-tested Api variant, so the contract value itself is independently verified; only the specific variant-to-arm binding for XmlRpc is unasserted.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-http-from-reqwest-untested",
      "kind": "coverage-gap",
      "title": "Http (reqwest) variant exit_code()==5 / error_type()==\"http\" never asserted",
      "location": "/Users/dave/src/bzr/src/error.rs:173,193",
      "description": "BzrError::Http(reqwest::Error) maps to EXIT_CODE_HTTP (5) / ERROR_TYPE_HTTP (\"http\"), shared with HttpStatus. Tests assert this mapping only for HttpStatus (error_tests.rs:88-96), never for the Http variant itself. The existing format_http_error_includes_source_chain test (line 164) constructs a real reqwest::Error but only feeds it to format_http_error() \u2014 it never wraps it in BzrError::Http nor calls exit_code()/error_type(). A regression that mapped a transport-level reqwest failure (the most common runtime error: connection refused, DNS failure, TLS handshake) to the wrong exit code would slip through.",
      "why_uncovered": "HttpStatus covers the shared arm, but the #[from] reqwest::Error path (Http) has no direct exit_code/error_type assertion. The one test touching a reqwest error checks message formatting only.",
      "severity": "low",
      "suggested_test": "In format_http_error_includes_source_chain (already builds a reqwest err), additionally assert: let bzr_err = BzrError::Http(err); assert_eq!(bzr_err.exit_code(), 5); assert_eq!(bzr_err.error_type(), \"http\");",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-http-from-reqwest-untested",
        "confirmed": true,
        "reason": "Verified gap is real. error_tests.rs contains NO construction of BzrError::Http(...) anywhere (grep returned nothing), and no test calls exit_code()/error_type() on the Http variant. The only test touching a real reqwest::Error is format_http_error_includes_source_chain (error_tests.rs:164-192), which builds the error at line 168-172 and passes it solely to format_http_error(&err) at line 181 \u2014 it never wraps it in BzrError::Http nor asserts the 5/\"http\" mapping. The exit_code_http_status test (error_tests.rs:88-96) asserts exit_code()==5 and error_type()==\"http\" only for the HttpStatus variant. Because Http(_) and HttpStatus{..} share one match arm (error.rs:173 and :193), the HttpStatus test indirectly exercises that arm, providing partial protection: a regression that altered the whole arm would be caught. The genuinely-uncovered risk is a regression splitting the arm so only BzrError::Http(_) (the most common transport failure: connection refused, DNS, TLS) maps to a different code \u2014 that would slip through, since HttpStatus's assertion would still pass. Real but narrow gap.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-tomlserialize-unproduced-untested",
      "kind": "coverage-gap",
      "title": "TomlSerialize variant has no exit_code/error_type test and no producer-failure test",
      "location": "/Users/dave/src/bzr/src/error.rs:169,189",
      "description": "BzrError::TomlSerialize maps to EXIT_CODE_CONFIG (3) / \"config\", grouped with Config and TomlParse. No test instantiates TomlSerialize and asserts its mapping. Its only producers are toml::to_string_pretty(self) at config.rs:265 and commands/config.rs:417 via the ? operator; neither has a test that forces serialization to fail and observes exit code 3. The variant's exit-code/error-type contract is entirely unverified \u2014 a mutation moving TomlSerialize out of the config arm (e.g. into Other => exit 1) would not be caught, and a config-save serialization failure would surface to the user with an unverified exit code.",
      "why_uncovered": "error_tests.rs tests TomlParse (lines 36,82) but not TomlSerialize. No config-save test triggers a serialization error path.",
      "severity": "low",
      "suggested_test": "Unit test in error_tests.rs constructing a TomlSerialize error (e.g. via toml::to_string of an unrepresentable value, or wrap a synthesized toml::ser::Error) and asserting exit_code()==3 and error_type()==\"config\".",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-tomlserialize-unproduced-untested",
        "confirmed": true,
        "reason": "Confirmed real coverage gap. error.rs:169,189 group BzrError::TomlSerialize into the config exit-code (3) / error_type (\"config\") arms, but no test verifies this. src/error_tests.rs tests TomlParse explicitly (exit_code_toml_parse lines 36-39 asserting exit 3; error_type_toml_parse lines 81-84 asserting \"config\") and covers Config/Api/Io/Other/NotFound/HttpStatus/InputValidation/Deserialize/Auth/DataIntegrity/Keyring/PinMismatch/IssuerChanged, but never constructs BzrError::TomlSerialize. A repo-wide grep shows TomlSerialize appears only at error.rs:22 (definition) and the two arm groupings (169,189) \u2014 zero test references. The only producers are toml::to_string_pretty(self)? at config.rs:265 and commands/config.rs:417; no test forces serialization to fail and observes exit code 3 (config_tests.rs:220 calls to_string_pretty().unwrap() merely to build valid input, not to exercise the failure path). No functional test references serialization either. A mutation moving TomlSerialize into a different arm (e.g. Other => exit 1) would not be caught. Severity refined to low: serialization of these simple, well-formed Config/ServerConfig structs is unlikely to fail in practice, so the practical blast radius is small; the gap is a missing contract assertion trivially fixable by mirroring the existing TomlParse tests.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-batchpartial-errortype-untested",
      "kind": "coverage-gap",
      "title": "BatchPartialFailure error_type()==\"batch_partial_failure\" never asserted",
      "location": "/Users/dave/src/bzr/src/error.rs:180,200",
      "description": "BatchPartialFailure exit_code()==11 is asserted indirectly only in attachment_tests.rs:1231 (download all-fail). Its error_type() (\"batch_partial_failure\", exit_code constant 11) is never asserted anywhere, and the bug-update producer (commands/bug/update.rs:243) builds the error in update_tests.rs:282 but does not assert the error_type string. Since error_type() feeds the machine-readable JSON error envelope (main.rs:88 \"type\" field), an incorrect type string for batch failures would ship to JSON consumers unnoticed. There is also no test asserting the Display message \"batch update: N succeeded, M failed\" for this variant.",
      "why_uncovered": "attachment_tests.rs:1231 checks exit==11 but not error_type; update_tests.rs constructs the variant for print_batch_result formatting, not for error_type/exit_code contract. error_tests.rs omits the variant entirely.",
      "severity": "low",
      "suggested_test": "Add to error_tests.rs: let err = BzrError::BatchPartialFailure { succeeded: 3, failed: 2 }; assert_eq!(err.exit_code(), 11); assert_eq!(err.error_type(), \"batch_partial_failure\"); assert_eq!(err.to_string(), \"batch update: 3 succeeded, 2 failed\");",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-batchpartial-errortype-untested",
        "confirmed": true,
        "reason": "Confirmed genuine coverage gap. error_tests.rs asserts error_type() for 13 variants (config@45, api@54, io@60, other@66, not_found@76, http@94, input@102, deserialize@110, auth@118, data_integrity@126, keyring@133, tls@145/157) but never BatchPartialFailure. attachment_tests.rs:1231 asserts only exit_code()==11, not error_type(). update_tests.rs:280-290 pattern-matches Err(BzrError::BatchPartialFailure{succeeded:1,failed:1}) and asserts JSON succeeded/failed fields, but never calls error_type() nor asserts the literal \"batch_partial_failure\". A repo-wide grep `rg error_type\\(\\) | rg batch` returns empty: error_type() is never invoked on a BatchPartialFailure anywhere in src/ or tests/, and functional run-tests.sh has no matches either. Since main.rs:88 wires error_type() directly into the JSON \"type\" field, a wrong string would ship to JSON consumers unnoticed. Additionally, no test asserts the Display string error.rs uses (\"batch update: {succeeded} succeeded, {failed} failed\"). Severity refined to low: the error_type()/exit_code() arms are single-line constant returns (error.rs:180,200) and exit_code is already indirectly covered, so regression risk is small even though the assertion is genuinely absent.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-no-distinctness-guard",
      "kind": "coverage-gap",
      "title": "No test guards that exit_code()/error_type() values are exhaustive and collision-free across all 18 variants",
      "location": "/Users/dave/src/bzr/src/error.rs:167,187",
      "description": "Each variant's mapping is tested in isolation (and several not at all). There is no single test that enumerates all variants and asserts the exit-code set is the intended {1,2,3,4,5,6,7,8,9,10,11,12,13} with the documented groupings, nor that error_type strings match. Because the enum is #[non_exhaustive] and grows over time (it has reached 18 variants / 13 codes), a newly added variant could silently fall into the wrong arm or duplicate a code without any failing test. The subsystem's core contract \u2014 every variant has a distinct, stable exit_code/error_type \u2014 is unenforced as a whole.",
      "why_uncovered": "Coverage is per-variant and incomplete (XmlRpc, Http, TomlSerialize, BatchPartialFailure missing). No aggregate/round-trip test exists; #[non_exhaustive] also prevents an exhaustive compile-time match check in tests.",
      "severity": "low",
      "suggested_test": "Add a table-driven test listing one constructed instance of every BzrError variant with its expected (exit_code, error_type) pair, asserting each, and asserting the multiset of codes matches the documented constants (catches both missing coverage and accidental arm collisions).",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-no-distinctness-guard",
        "confirmed": true,
        "reason": "Verified against /Users/dave/src/bzr/src/error_tests.rs (the sole sibling test file). Every exit_code()/error_type() test is a per-variant isolated assertion (e.g. exit_code_config:6, exit_code_api:12, exit_code_http_status:88, exit_code_pin_mismatch:138). There is NO aggregate test that enumerates all variants, asserts the exit-code set equals the documented {1..13}, or checks for collisions/exhaustiveness. The claim that four variants lack any exit_code/error_type coverage is confirmed: (1) Http \u2014 only format_http_error_includes_source_chain (line 164) tests the message chain, never .exit_code()/.error_type(); (2) TomlSerialize \u2014 never constructed in any test; (3) XmlRpc \u2014 only appears in is_bug_get_per_resource_false_for_transport_and_auth (line 255), never asserts exit_code 4 / type \"api\"; (4) BatchPartialFailure \u2014 not referenced in error_tests.rs at all, no assertion of exit_code 11 / type \"batch_partial_failure\". Other .exit_code() call sites (tests/integration.rs:2493, main_tests.rs:200-201, command _tests files) are per-command behavior checks, not the enum-wide contract. Because the enum is #[non_exhaustive] with grouped match arms (Config/TomlParse/TomlSerialize->3, Api/XmlRpc->4, Http/HttpStatus->5, PinMismatch/IssuerChanged->13 at error.rs:167-205), a new variant could be mis-grouped or duplicate a code with no failing test. Severity is low rather than high: the present mapping at error.rs:167 and 187 is correct and collision-free, so this is a regression-prevention/coverage gap, not an active defect.",
        "refined_severity": "low"
      }
    },
    {
      "id": "error-exitcodes-stale-range-comment",
      "kind": "likely-bug",
      "title": "main.rs exit_code comment claims range 1..=12 but TLS variants exit 13",
      "location": "/Users/dave/src/bzr/src/main.rs:73,75",
      "description": "The doc comment says \"Convert a BzrError exit code (1-12) to a std::process::ExitCode\" and the inline comment says \"All BzrError exit codes are in the range 1..=12.\" but error.rs:102 defines EXIT_CODE_TLS = 13 (PinMismatch/IssuerChanged). The runtime is safe because u8::try_from(13) succeeds and unwrap_or(1) is only a fallback, but the comment is factually wrong and misleading: a reader could assume 13 is impossible. If the enum keeps growing past 255 the unwrap_or(1) silently collapses to exit 1, and the stale upper bound makes that footgun harder to spot. No test asserts that a TLS error (exit 13) survives the exit_code() conversion in main.rs (only error_tests.rs asserts BzrError::*.exit_code()==13, not the ExitCode::from path).",
      "why_uncovered": "main_tests.rs exit_code_maps_* tests only exercise InputValidation (7) and Other (1); no test passes a value at or near the real upper bound (13) through exit_code() in main.rs, so the stale range and the truncation fallback are both unexercised.",
      "severity": "low",
      "suggested_test": "Update both comments to 1..=13, and add a main_tests.rs case: exit_code(&BzrError::PinMismatch{..}) renders an ExitCode whose Debug contains \"13\".",
      "subsystem": "error-exitcodes",
      "verdict": {
        "id": "error-exitcodes-stale-range-comment",
        "confirmed": true,
        "reason": "Confirmed. main.rs:73 doc comment says exit codes are \"(1-12)\" and main.rs:75 inline comment says \"All BzrError exit codes are in the range 1..=12.\" but error.rs:102 defines EXIT_CODE_TLS = 13, returned by PinMismatch/IssuerChanged (error.rs:182). The comment is factually wrong. Test coverage gap is real: main_tests.rs only routes InputValidation (7) via exit_code_maps_known_error_to_exit_code and Other (1) via exit_code_maps_other_variant through main.rs::exit_code(); no test passes a value at/near 13 through that ExitCode::from(u8::try_from(...).unwrap_or(1)) path. error_tests.rs:138-156 (exit_code_pin_mismatch, exit_code_issuer_changed) assert only BzrError::*.exit_code()==13 on error.rs's method, not the main.rs conversion. The runtime is correct (u8::try_from(13) succeeds; unwrap_or(1) is unreachable until codes exceed 255), so this is a misleading-comment defect with a latent truncation footgun, not a live bug \u2014 hence low severity.",
        "refined_severity": "low"
      }
    },
    {
      "id": "output-mask-api-key-non-ascii-panic",
      "kind": "likely-bug",
      "title": "mask_api_key panics on a non-ASCII inline API key (byte-index slice, not char boundary)",
      "location": "src/output/formatting.rs:117-123",
      "description": "mask_api_key uses `key.len() > 8` (byte length) and then `&key[..8]` (byte slice). If the configured inline API key contains a multi-byte UTF-8 char straddling byte offset 8, the slice is not on a char boundary and the program panics ('byte index 8 is not a char boundary'). I reproduced the panic with a 10-byte key '1234567\u00e99'. This is reachable end-to-end: `config show` renders every server via ServerDisplayInfo::from_config (src/output/resources/config.rs:36-37), which calls mask_api_key on the stored inline key. A user is free to put a non-ASCII string in the api_key field. The companion `truncate` helper guards Unicode by counting chars; mask_api_key does not. Fix: take chars (`key.chars().take(8).collect()`) and compare char count, or use `key.char_indices()`.",
      "why_uncovered": "formatting_tests.rs:166-183 covers mask_api_key only with ASCII inputs (long key, short key, exactly-8, empty). config_tests.rs uses the ASCII key '1234567890abcdef'. No test feeds a multi-byte key, so the char-boundary panic path is never exercised.",
      "severity": "medium",
      "suggested_test": "Unit test in formatting_tests.rs: `assert_eq!(mask_api_key(\"1234567\u00e99abc\"), \"1234567\u00e9...\")` (or whatever the correct char-based prefix is) and one with a leading multibyte char; assert no panic and a sensible masked prefix. Add a config_tests.rs case constructing a ServerConfig with a non-ASCII inline api_key and asserting from_config does not panic.",
      "subsystem": "output",
      "verdict": {
        "id": "output-mask-api-key-non-ascii-panic",
        "confirmed": true,
        "reason": "Genuine bug. src/output/formatting.rs:118-119 checks byte length (`key.len() > 8`) then byte-slices (`&key[..8]`); if a multi-byte UTF-8 char straddles byte offset 8 this slices off a char boundary and panics. I reproduced the exact panic with key \"1234567\u00e99\": \"end byte index 8 is not a char boundary; it is inside '\u00e9' (bytes 7..9)\". It is reachable end-to-end: ServerDisplayInfo::from_config (src/output/resources/config.rs:36-37) calls mask_api_key on the raw inline key returned verbatim by Config::credential_source -> CredentialSource::Inline (src/config.rs:142-143), with no ASCII validation; `config show` renders every server this way. Tests do NOT cover it: formatting_tests.rs:166-183 (mask_api_key_long_key_shows_prefix, _short_key_fully_masked, _exactly_8_chars_fully_masked, _empty_string_fully_masked) all use ASCII inputs, and config_tests.rs:45 and :147 use the ASCII key \"1234567890abcdef\". No test feeds a multi-byte key, so the char-boundary panic path is unexercised. The sibling `truncate` helper guards Unicode by counting chars while mask_api_key does not, confirming the inconsistency. Fix: use key.chars().take(8) / count chars, or key.char_indices().",
        "refined_severity": "medium"
      }
    },
    {
      "id": "output-server-info-render-untested",
      "kind": "coverage-gap",
      "title": "write_server_info table rendering (no-extensions branch, 'unknown' version fallback) is never executed",
      "location": "src/output/resources/server.rs:26-44",
      "description": "write_server_info contains three render branches: the 'No extensions installed.' line (server.rs:35), the extensions loop with an `ext.version.as_deref().unwrap_or(\"unknown\")` fallback for a missing extension version (server.rs:38-41), and the bolded version header (server.rs:33). None of these are exercised. server_tests.rs only constructs the internal `ServerInfo` struct and asserts its field values (e.g. `info.extensions.is_empty()`) without ever calling write_server_info, so the actual writeln! formatting, the empty-extensions message, and the None-version 'unknown' fallback are all uncovered. A mutation that deletes the 'No extensions installed.' arm or changes the 'unknown' literal would not be caught.",
      "why_uncovered": "server_tests.rs:5-32 builds `super::ServerInfo {...}` and asserts struct fields; print_server_info_json_combined builds a raw serde_json::json! value rather than calling the function. The function under test (write_server_info) is invoked by no unit or integration test.",
      "severity": "low",
      "suggested_test": "Add server_tests.rs cases that call write_server_info into a Vec<u8> buffer for: (a) a ServerInfoResponse with no extensions (assert output contains 'No extensions installed.' and the version), (b) an extension whose ExtensionInfo.version is None (assert output contains '(unknown)'), (c) JSON format (assert valid JSON with version + extensions keys).",
      "subsystem": "output",
      "verdict": {
        "id": "output-server-info-render-untested",
        "confirmed": true,
        "reason": "Verified real gap. write_server_info's Table-mode closure (src/output/resources/server.rs:33-42) holds the three render branches: bold version header (line 33), \"No extensions installed.\" (line 35), and the extensions loop with ext.version.as_deref().unwrap_or(\"unknown\") fallback (lines 38-41). write_formatted (src/output/formatting.rs:27-30) only invokes the table_fn closure for OutputFormat::Table; OutputFormat::Json routes to write_json and skips the closure entirely. The only test calling write_server_info is server_info_integration (tests/integration.rs:365-400), which passes OutputFormat::Json (line 391), so the Table closure never executes. The unit tests server_tests.rs:5-32 only construct super::ServerInfo and assert struct fields (e.g. info.extensions.is_empty()) without calling write_server_info; the third test (server_tests.rs:34-59) builds a raw serde_json::json! value and never calls the function. The functional test (tests/functional/run-tests.sh:127-129) runs `server info` via run_bzr, which always injects --json (tests/functional/lib.sh:119), again bypassing the Table branches. A mutation deleting the \"No extensions installed.\" arm, altering the \"unknown\" literal, or removing the version header would not be caught by any cargo test. Severity low: purely cosmetic table formatting with no logic/data impact.",
        "refined_severity": "low"
      }
    },
    {
      "id": "output-fields-exclude-combined-untested",
      "kind": "coverage-gap",
      "title": "No test combines --fields and --exclude-fields together on any bug output path",
      "location": "src/output/resources/bug.rs:328-340 (bug_to_json), 404-416 (field_selected), 276-304 (validate_table_columns)",
      "description": "Every bug field-selection test sets exactly one of include/exclude. There is no test where ColumnSpec has both include: Some(..) and exclude: Some(..). The interaction matters: bug_to_json first retains the include keep-set then removes excludes (so excluding a field that is also included must drop it); field_selected computes `included && !excluded` (detail view); validate_table_columns and validate_json_field_selection both build the include set then apply_exclude/remove and only error if the result is empty. A bug where exclude is computed before include, or where exclude is ignored when include is present, would pass all current tests. The combined case (e.g. include='id,status,priority', exclude='status') is the realistic gh-style usage and is unverified across JSON projection, table column resolution, detail-row selection, and the pre-network validators.",
      "why_uncovered": "bug_tests.rs include-only tests (bug_to_json_include_keeps_only_named_keys, detail_include_limits_rows, write_bugs_fields_selects_only_requested_columns) and exclude-only tests (bug_to_json_exclude_id_drops_id, detail_exclude_drops_row, write_bugs_exclude_fields_drops_default_column) never set both fields simultaneously; a grep for include+exclude both Some returns nothing.",
      "severity": "medium",
      "suggested_test": "Parametrized tests: bug_to_json with include='id,status,priority' + exclude='status' yields keys [id, priority]; write_bugs table with the same spec shows ID/PRIORITY but not STATUS; field_selected returns false for 'status' under that spec; validate_table_columns/validate_json_field_selection succeed (one column remains) and error when exclude removes every included field.",
      "subsystem": "output",
      "verdict": {
        "id": "output-fields-exclude-combined-untested",
        "confirmed": true,
        "reason": "Verified against /Users/dave/src/bzr/src/output/resources/bug_tests.rs. The realistic combined case (include a set AND exclude a subset, expecting the remainder to survive) is genuinely untested on three of the four paths: bug_to_json (all tests set exactly one \u2014 include-only at lines 727-731,737-741,826-828,839-841; exclude-only at 749-753,763-767), field_selected/detail (include-only detail_include_limits_rows line 431-435, exclude-only detail_exclude_drops_row line 447-451), and validate_json_field_selection (all single \u2014 881-884,891-899,906-912,918-921,927-935). Functional tests (tests/functional/run-tests.sh:424-426, 950) only ever pass --fields, never combined with --exclude-fields. The ONE test that sets both Some is validate_table_columns_errors_when_exclude_removes_sole_include (line 672-680: include='id', exclude='id'), so the claim's assertion that 'a grep for include+exclude both Some returns nothing' is inaccurate \u2014 but that test is a degenerate full-overlap case that only checks the table validator's error path; it does NOT verify that a non-overlapping remainder survives, nor does it touch the JSON projection, detail selection, or JSON validator. The code at bug.rs:331-337 is actually correct (retain include keep-set, then remove excludes \u2014 exclude applied after include), so this is a coverage gap, not a live bug. A regression that ignored exclude when include is present, or reordered the two, would pass all current tests on the three uncovered paths. Severity is medium because the underlying logic is simple/correct and only the regression-guard is missing, and one partial combined test does exist.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "output-truncate-small-bound-underflow",
      "kind": "likely-bug",
      "title": "truncate panics (subtract-with-overflow) when max_chars < 3 and input is longer",
      "location": "src/output/formatting.rs:91-98",
      "description": "truncate computes `max_chars - 3` on a usize when the string is longer than max_chars. For max_chars < 3 this underflows: in debug builds (cargo test) it panics with 'attempt to subtract with overflow' (reproduced with truncate(\"abcdefg\", 2)); in release it wraps to a huge `take`, silently returning the whole string plus '...'. All current call sites pass fixed constants (72 for bug summary at bug.rs:72, 60 for product description at product.rs:69), so it is not reachable in production today, but it is an unguarded shared helper with a latent panic and an inconsistent debug/release contract. Fix: `let keep = max_chars.saturating_sub(3);` or special-case max_chars <= 3.",
      "why_uncovered": "formatting_tests.rs:8-32 tests truncate at boundaries 5/7/10 and the Unicode case at 3 and 4, but never below 3 with an over-length input, so the underflow/panic branch is untested. The test at line 30 (truncate(input,3)) exactly hits max_chars==3 (3-3==0, no underflow) and stops short of the failing range.",
      "severity": "low",
      "suggested_test": "Add truncate edge tests for max_chars of 0, 1, 2 with an over-length input (e.g. truncate(\"abcdefg\", 2)); assert it returns a defined value rather than panicking. This both documents the contract and would fail today, prompting a saturating_sub fix.",
      "subsystem": "output",
      "verdict": {
        "id": "output-truncate-small-bound-underflow",
        "confirmed": true,
        "reason": "Code at src/output/formatting.rs:93 computes `max_chars - 3` on a usize inside the `s.chars().count() > max_chars` branch. For max_chars < 3 with an over-length input this underflows: debug builds panic (\"attempt to subtract with overflow\"), release wraps to a huge `take`. The coverage gap is real: in src/output/formatting_tests.rs the truncate tests only use max_chars >= 3 with over-length inputs. The lowest is line 30 `truncate(input, 3)` where input has 4 chars \u2014 hits `3-3=0`, no underflow. Lines 10/15/28 are not over-length (so the subtraction branch isn't entered), and line 20 uses max_chars=7. No test passes max_chars in {0,1,2} with a longer string, so the underflow/panic branch is untested. However severity is low: `truncate` is `pub(super)` (module-private, not public API), and all three production call sites pass fixed constants well above 3 \u2014 bug.rs:72 (72), product.rs:69 (60), classification.rs:24 (60) \u2014 so the branch is unreachable in production today. It is a genuine latent defect with an inconsistent debug/release contract in an internal helper, fixable with `max_chars.saturating_sub(3)`.",
        "refined_severity": "low"
      }
    },
    {
      "id": "output-colorize-status-lowercase-no-ansi-assert",
      "kind": "coverage-gap",
      "title": "colorize_status uppercase-normalization for lowercase input is not asserted to actually colorize",
      "location": "src/output/formatting.rs:108-115",
      "description": "colorize_status matches on `status.to_uppercase()`, so lowercase 'new'/'resolved' should still be colored. The ANSI-escape assertion test (formatting_tests.rs:67-96) only feeds UPPERCASE statuses, and colorize_status_case_insensitive (line 60-64) only asserts the output *contains* the literal 'new' \u2014 not that an ANSI escape was emitted. A mutation that drops the `.to_uppercase()` normalization (matching raw input) would leave lowercase statuses uncolored yet still pass every test, since the content check succeeds and the ANSI test uses only uppercase inputs.",
      "why_uncovered": "colorize_status_known_statuses_emit_ansi_escapes iterates only uppercase variants; colorize_status_case_insensitive checks substring presence, not ANSI presence. No test forces color override AND feeds a lowercase known status while asserting '\\x1b[' is present.",
      "severity": "low",
      "suggested_test": "Extend the color-override test (or add a sibling) that sets colored::control::set_override(true) and asserts colorize_status(\"new\") and colorize_status(\"resolved\") each contain '\\x1b[' \u2014 proving the to_uppercase() normalization drives the color path.",
      "subsystem": "output",
      "verdict": {
        "id": "output-colorize-status-lowercase-no-ansi-assert",
        "confirmed": true,
        "reason": "Confirmed real coverage gap. colorize_status (src/output/formatting.rs:108-115) matches on status.to_uppercase(), so lowercase known statuses should be colored. The only ANSI-asserting test, colorize_status_known_statuses_emit_ansi_escapes (formatting_tests.rs:67-96), iterates ONLY uppercase variants [\"NEW\",\"UNCONFIRMED\",...] under set_override(true); a mutation dropping .to_uppercase() leaves uppercase inputs still matching the literal arms, so this test passes. The only lowercase test, colorize_status_case_insensitive (formatting_tests.rs:60-64), feeds \"new\" but asserts only result.contains(\"new\") \u2014 under the mutation \"new\" falls to the catch-all `_ => status.to_string()` returning \"new\", which still contains \"new\", so it also passes. No test forces color override AND feeds a lowercase known status while asserting \"\\x1b[\" presence. rg over the whole tree confirms colorize_status is only tested in formatting_tests.rs (the sole production caller is src/output/resources/bug.rs:468, untested for color). Severity is low: the undetectable regression is cosmetic (uncolored lowercase status text), no correctness/data impact.",
        "refined_severity": "low"
      }
    },
    {
      "id": "validation-parsing-deadline-unvalidated",
      "kind": "likely-bug",
      "title": "--deadline accepts any string; no client-side date validation (no exit 7 on malformed)",
      "location": "src/commands/bug/update.rs:146",
      "description": "build_update_params copies the raw `--deadline` value into UpdateBugParams verbatim (`deadline: deadline.clone()`), with no call to validation::parse_iso8601_or_date / parse_optional_date. Every other date-shaped CLI flag in the codebase (--created-since, --changed-since, --since for both bug history and comment list, and query save's --created-since/--changed-since) is validated client-side and fails fast with BzrError::InputValidation (exit 7). The deadline doc string (src/cli/bug.rs:646 'Set the deadline date (YYYY-MM-DD)') and docs/bzr-cli.md:496 advertise the same YYYY-MM-DD shape, and the subsystem focus explicitly lists `--deadline` malformed -> exit 7 as expected behavior. As written, `bzr bug update 12345 --deadline garbage` ships the garbage string to the Bugzilla server instead of failing fast, producing an inconsistent/opaque server-side error (or silent acceptance) rather than the documented client-side validation. This is an inconsistency bug: the validation primitive exists and is used everywhere else, but was not wired into the deadline path.",
      "why_uncovered": "update_tests.rs only exercises the happy path: line 91 sets deadline Some(\"2026-12-31\") and line 352 asserts it round-trips into params unchanged. Functional test 42a (run-tests.sh:472) likewise only runs `--deadline 2026-12-31`. No test passes a malformed deadline, so the missing validation is invisible. cli/mod_tests.rs:391-422 is a pure clap-parse test (string lands in struct), not a validation test.",
      "severity": "medium",
      "suggested_test": "Unit test in a new src/commands/bug/update_tests.rs case: call build_update_params with a BugAction::Update whose deadline = Some(\"tomorrow\") (and separately \"2026-13-99\") and assert it returns Err(BzrError::InputValidation(_)) with exit_code()==7. After wiring parse_optional_date(deadline, \"--deadline\") into update.rs, add a functional test mirroring 45b: `bzr bug update $BUG --deadline tomorrow` asserting exit 7.",
      "subsystem": "validation-parsing",
      "verdict": {
        "id": "validation-parsing-deadline-unvalidated",
        "confirmed": true,
        "reason": "Confirmed real gap. src/commands/bug/update.rs:146 does `deadline: deadline.clone()` with zero validation; reading the whole build_update_params (lines 99-190) shows it validates string lists, comments, and alias but never the deadline. The validation primitive exists (src/validation/datetime.rs:23 parse_iso8601_or_date, wrapped as parse_optional_date in src/validation/mod.rs:20) and rejects garbage like \"tomorrow\"/\"2026-13-01\" per datetime_tests.rs. Every other date flag is validated client-side: bug/list.rs:44-45 (--created-since/--changed-since), bug/history.rs:19 (--since), comment.rs:26 (--since), query.rs:75/77/214/216 \u2014 but the deadline path bypasses it. No test covers a malformed deadline: update_tests.rs only exercises the happy path (line 91 sets deadline Some(\"2026-12-31\"), line 352 asserts round-trip unchanged); functional test case 42a (run-tests.sh:472-477) only runs the valid `--deadline 2026-12-31`. So `bzr bug update --deadline garbage` does not fail fast with exit 7 as documented (cli/bug.rs:646, docs/bzr-cli.md:496).",
        "refined_severity": "medium"
      }
    },
    {
      "id": "validation-parsing-url-limit-silently-dropped",
      "kind": "likely-bug",
      "title": "Invalid `limit=` URL param is silently dropped; `limit=0` silently accepted",
      "location": "src/url_parser.rs:162-166",
      "description": "In parse_bugzilla_url, ParamKind::Limit handling is `if let Ok(n) = value.parse::<u32>() { query.limit = Some(n); }`. A non-numeric or out-of-range limit (e.g. `limit=abc`, `limit=-5`, `limit=99999999999`) is silently discarded with no warning and no error \u2014 the resulting SavedQuery ends up with limit=None, which later means 'server default' rather than the user's intent. Contrast with credential params, which at least emit a tracing::warn. Additionally `limit=0` parses to Some(0) and is accepted, which is almost certainly not a meaningful page size and may behave inconsistently across Bugzilla versions. Given the file's stated philosophy of failing fast at the CLI for malformed input (see datetime.rs module docs), silently swallowing a malformed limit is surprising.",
      "why_uncovered": "url_parser_tests.rs only tests valid numeric limits (line 88 limit=50, line 220 limit=100) and asserts the matching Some value. No test passes a non-numeric, negative, zero, or overflowing limit, so the silent-drop branch and the limit=0 acceptance are entirely unexercised.",
      "severity": "low",
      "suggested_test": "Add url_parser_tests cases: parse_test_url(\"limit=abc\") and parse_test_url(\"limit=999999999999\") asserting either an InputValidation error or, if silent-drop is intended, explicitly assert query.limit==None AND assert a warning is logged. Add parse_test_url(\"limit=0\") asserting the chosen behavior. Decide and document whether malformed limit should error (exit 7) for consistency with date validation.",
      "subsystem": "validation-parsing",
      "verdict": {
        "id": "validation-parsing-url-limit-silently-dropped",
        "confirmed": true,
        "reason": "Confirmed at src/url_parser.rs:162-166: ParamKind::Limit uses `if let Ok(n) = value.parse::<u32>() { query.limit = Some(n); }`, which silently discards non-numeric/negative/overflowing values (no warn, no error) and accepts limit=0 as Some(0). The adjacent ParamKind::Credential branch (line 177) emits tracing::warn, highlighting the inconsistency. Tests in src/url_parser_tests.rs only cover valid limits: limit=50 asserting Some(50) (lines 88-91) and limit=100 asserting Some(100) (lines 220-229). parse_malformed_url_errors (line 146-153) tests only a wholly invalid URL string (\"not a url\"), not a malformed limit value. No test passes limit=abc, limit=-5, limit=0, or an overflowing value, so the silent-drop branch and limit=0 acceptance are unexercised. Functional tests (tests/functional/run-tests.sh lines 434, 908-946) exercise only the --limit CLI flag with valid integers, not URL parsing. Severity is low: a dropped limit falls back to the server default (a UX surprise, not data corruption/security), the input is fully recoverable, and limit=0 is an unlikely edge case.",
        "refined_severity": "low"
      }
    },
    {
      "id": "validation-parsing-date-year-zero-bounds",
      "kind": "coverage-gap",
      "title": "No bounds test for year 0000 / extreme years in date parser",
      "location": "src/validation/datetime.rs:50-69",
      "description": "parse_date accepts any 4-digit year 0000-9999 with no lower bound, so `0000-01-01` is canonicalized to `0000-01-01T00:00:00Z` and `is_leap_year(0)` returns true (0%4==0 && 0%400==0), so `0000-02-29` is also accepted. Bugzilla will reject these as search filters, defeating the module's stated goal of failing fast at the CLI rather than shipping a payload the server rejects. The boundary years (0000, and the leap-year logic at year boundaries like 1900 non-leap / 2000 leap) are never asserted.",
      "why_uncovered": "datetime_tests.rs covers leap/non-leap for 2024/2025 and Feb-29 specifically, plus month/day bounds, but never asserts behavior for year 0000 or century-boundary leap rules (1900 should be non-leap, 2000 leap). The is_leap_year century logic (line 83) is only indirectly tested via 2024/2025/2026.",
      "severity": "low",
      "suggested_test": "Add datetime_tests cases: assert is_leap_year via parse for \"1900-02-29\" is_err (century non-leap), \"2000-02-29\" is_ok (400-divisible leap), and decide/assert behavior for \"0000-01-01\" (recommend rejecting years below a sensible floor like 1900 to fail fast).",
      "subsystem": "validation-parsing",
      "verdict": {
        "id": "validation-parsing-date-year-zero-bounds",
        "confirmed": true,
        "reason": "Verified against the sole coverage source, src/validation/datetime_tests.rs (the only test file touching parse_iso8601_or_date; rg confirms tests/functional/run-tests.sh and the functional suite never exercise date parsing). The leap-year tests use only 2024 (rejects_february_twenty_ninth... line 120, accepts... line 125), 2025 (line 121), 2016 (accepts_leap_second line 40), and 2026 elsewhere. None of these years is divisible by 100, so the century-rule branches of is_leap_year at src/validation/datetime.rs:83 (the `!year.is_multiple_of(100) || year.is_multiple_of(400)` clauses) are never exercised \u2014 directly or indirectly. A mutation dropping the `|| year.is_multiple_of(400)` clause (which would wrongly reject 2000-02-29) or the `!...is_multiple_of(100)` clause (which would wrongly accept 1900-02-29) survives the entire suite. That is a genuine, mutation-surviving coverage gap on the trickiest branch of the leap-year logic. The claim's separate assertion that year 0000 is an actual bug is weaker (0000 is structurally valid ISO-8601 and the module documents no minimum-year contract, so accepting it is defensible), but the central verifiable point \u2014 century-boundary leap rules (1900/2000) are never asserted \u2014 holds.",
        "refined_severity": "low"
      }
    },
    {
      "id": "tls-keyring-map-error-untested-variants",
      "kind": "coverage-gap",
      "title": "Keyring error mapping only tests NoEntry; 6 other variants and fallback are uncovered",
      "location": "/Users/dave/src/bzr/src/credentials/keyring.rs:86-115",
      "description": "map_error() produces distinct user-facing guidance for KrError::PlatformFailure, NoStorageAccess, TooLong, Ambiguous, BadEncoding, Invalid, and the catch-all `other` arm. These are the headless/CI and corruption-handling messages users will actually hit when the OS keychain is locked, unavailable, or a server name is too long. The only mapping exercised by tests is the NoEntry branch (keyring_tests.rs: retrieve_missing_entry_maps_to_no_entry_message and the roundtrip). A wrong/empty message string on any of these arms (e.g. dropping the `api_key_env` remediation hint) would ship silently. The in-process test_store only ever returns NoEntry, so it cannot exercise the other arms at all.",
      "why_uncovered": "keyring_tests.rs covers store/retrieve/delete roundtrip and the NoEntry path via the in-process test_store. The test_store (keyring.rs:199-236) can only return NoEntry or PlatformFailure-on-poison; it never returns TooLong/Ambiguous/NoStorageAccess/BadEncoding/Invalid, so those map_error arms have zero coverage and the message text is unverified.",
      "severity": "low",
      "suggested_test": "Add unit tests that call map_error(\"svc\",\"acct\",&KrError::<variant>) directly for each of PlatformFailure, NoStorageAccess, TooLong, Ambiguous, BadEncoding, Invalid, and a generic `other`, asserting the message contains the expected guidance substring (e.g. \"api_key_env\" for PlatformFailure/NoStorageAccess, \"exceeds platform limit\" for TooLong, \"remove duplicates\" for Ambiguous, \"corrupted\" for BadEncoding/Invalid).",
      "subsystem": "tls-keyring",
      "verdict": {
        "id": "tls-keyring-map-error-untested-variants",
        "confirmed": true,
        "reason": "Verified the gap is real. The only tests are the three in /Users/dave/src/bzr/src/credentials/keyring_tests.rs: store_retrieve_delete_roundtrip (line 6), retrieve_missing_entry_maps_to_no_entry_message (line 21), and delete_missing_entry_is_ok (line 29). All three exercise only the NoEntry arm of map_error (keyring.rs:88) via assertions on \"no API key found\". The in-process test_store (keyring.rs:199-227) can only return Error::NoEntry from get_secret/delete_credential/get_credential, or Error::PlatformFailure on mutex poison (line 191) \u2014 and no test triggers poisoning. Thus the PlatformFailure (line 92), NoStorageAccess (line 96), TooLong (line 100), Ambiguous (line 105), BadEncoding/Invalid (line 109), and the `other` catch-all (line 112) arms have zero coverage; their message text (including the api_key_env remediation hints) is unverified. rg confirms map_error is referenced only inside keyring.rs (no other test file constructs KrError variants), and the functional test tests/functional/keyring-test.sh exercises only the happy path (set-keyring/migrate/show/unset). Severity is low: these arms produce static diagnostic format strings, not logic \u2014 a regression degrades error guidance text rather than causing incorrect behavior or data loss, and the strings are unlikely to break silently.",
        "refined_severity": "low"
      }
    },
    {
      "id": "tls-keyring-issuer-with-comma-pin-mismatch",
      "kind": "coverage-gap",
      "title": "PIN_MISMATCH classification with a multi-RDN issuer DN containing commas is untested",
      "location": "/Users/dave/src/bzr/src/tls/pin_failure.rs:40-51",
      "description": "parse_pin_mismatch_details treats everything after the literal \", issuer \" as the issuer string. Real issuer DNs from extract_issuer_dn are formatted \"CN=foo, O=bar, C=US\" (extract_rdns joins with \", \"). The only test (pin_failure_tests.rs:6) uses a single-RDN issuer \"CN=Test CA\" with no internal commas. A multi-RDN issuer is the common production case; if reqwest ever appends trailing context after the issuer in the error chain, the greedy trailing-capture would also swallow it into new_issuer. Neither the multi-comma issuer nor any trailing-text scenario is asserted.",
      "why_uncovered": "pin_failure_tests.rs::classifies_pin_mismatch_chain_into_typed_error uses issuer \"CN=Test CA\" (no embedded commas, nothing trailing). The realistic multi-RDN/comma case is not exercised.",
      "severity": "low",
      "suggested_test": "Add classify_chain test with chain `...PIN_MISMATCH for s: expected sha256//a==, got sha256//b==, issuer CN=foo, O=Acme, C=US` and assert new_issuer == \"CN=foo, O=Acme, C=US\".",
      "subsystem": "tls-keyring",
      "verdict": {
        "id": "tls-keyring-issuer-with-comma-pin-mismatch",
        "confirmed": true,
        "reason": "The only PIN_MISMATCH test, pin_failure_tests.rs:6 (classifies_pin_mismatch_chain_into_typed_error), uses a single-RDN issuer \"CN=Test CA\" with no embedded commas (line 8). No test exercises a multi-RDN issuer like \"CN=foo, O=bar, C=US\" that extract_rdns actually produces (verifier.rs:371 joins RDNs with \", \"). So the multi-comma issuer path through parse_pin_mismatch_details (pin_failure.rs:40-51) is genuinely uncovered \u2014 confirming the coverage gap. HOWEVER, the code is correct for that case: the markers \", got \" (line 44) and \", issuer \" (line 47, with the literal word \"issuer\") do not collide with RDN separators like \", O=bar\", and the greedy trailing capture on line 49 correctly grabs the full multi-RDN DN because the producer at verifier.rs:144 (\"PIN_MISMATCH for {}: expected {}, got {}, issuer {}\") places the issuer last with nothing after it. The claim's \"trailing context\" failure mode is explicitly hypothetical (\"if reqwest ever appends...\") and cannot occur given the producer's format. So this is a real missing-test-case (add a multi-RDN assertion), not a latent bug.",
        "refined_severity": "low"
      }
    },
    {
      "id": "tls-keyring-issuer-der-extraction-no-tag-check",
      "kind": "likely-bug",
      "title": "extract_issuer_der does not validate the issuer is a SEQUENCE, unlike the string path",
      "location": "/Users/dave/src/bzr/src/tls/verifier.rs:264-269",
      "description": "extract_issuer_der calls skip_der_element(pos) on the byte position returned by navigate_to_issuer without first checking that the tag at pos is 0x30 (SEQUENCE). parse_issuer_from_tbs (line 296-297) DOES validate via parse_der_sequence and returns None on a non-SEQUENCE tag. For a malformed/truncated cert where navigate_to_issuer lands on a non-SEQUENCE element, extract_issuer_der returns Some(bytes) of whatever element is there while extract_issuer_dn returns the \"<raw DER>\" fallback. This means the DER-pin path (check_issuer_change DER branch, verifier.rs:95-108) and the legacy string-pin path can disagree about whether the issuer changed for the same bytes. The DER branch could even silently accept (Ok(None)) a cert whose issuer the string parser couldn't even read.",
      "why_uncovered": "verifier_tests.rs only feeds extract_issuer_der well-formed rcgen certs (extract_issuer_der_returns_consistent_bytes / _differs_for_different_issuers) and one all-garbage input that fails navigate_to_issuer early (extract_issuer_der_returns_none_for_garbage). No test crafts a cert whose TBS is valid up to the issuer position but has a non-SEQUENCE element there, so the missing tag check is invisible.",
      "severity": "low",
      "suggested_test": "Construct a minimal DER byte sequence: outer SEQUENCE -> TBS SEQUENCE containing valid serial INTEGER + signature SEQUENCE, but place a non-SEQUENCE element (e.g. an OCTET STRING 0x04) where the issuer SEQUENCE should be. Assert extract_issuer_der returns None (after adding a 0x30 tag check) and that it agrees with extract_issuer_dn returning the fallback string.",
      "subsystem": "tls-keyring",
      "verdict": {
        "id": "tls-keyring-issuer-der-extraction-no-tag-check",
        "confirmed": true,
        "reason": "The code asymmetry is real and genuinely untested. extract_issuer_der (verifier.rs:264-269) calls skip_der_element(pos) without checking pos starts with 0x30, whereas parse_issuer_from_tbs (verifier.rs:296-297) validates via parse_der_sequence (which enforces the 0x30 tag at verifier.rs:305) and returns None on a non-SEQUENCE. navigate_to_issuer (verifier.rs:245-260) validates only the outer/TBS SEQUENCEs and uses tag-agnostic skip_der_element for version/serial/signature, so it can return Some(pos) where pos points at a non-SEQUENCE element. In that case extract_issuer_der returns Some(arbitrary element bytes) while extract_issuer_dn returns the \"<raw DER>\" fallback \u2014 the two paths disagree. Tests confirm this is uncovered: verifier_tests.rs only feeds extract_issuer_der well-formed rcgen certs (extract_issuer_der_returns_consistent_bytes:174, extract_issuer_der_differs_for_different_issuers:183) and one all-garbage input that fails navigate_to_issuer's outer parse_der_sequence early (extract_issuer_der_returns_none_for_garbage:192). No test crafts a cert valid up to the issuer position but holding a non-SEQUENCE there, and no functional test exercises issuer DER (grep of tests/ returns nothing). HOWEVER, the description's \"could silently accept (Ok(None)) a cert\" claim is wrong: verify_server_cert (verifier.rs:124-147) returns Ok ONLY on hash match (line 135-136); check_issuer_change returning Ok(None) falls through to the unconditional Err(PIN_MISMATCH) at line 143. The divergence therefore only affects which error message is emitted (ISSUER_CHANGED vs PIN_MISMATCH), never the accept/reject decision \u2014 so this is a low-severity consistency/robustness gap, not a security bypass.",
        "refined_severity": "low"
      }
    },
    {
      "id": "tls-keyring-no-cert-captured-error-untested",
      "kind": "coverage-gap",
      "title": "probe_server_cert 'no certificate captured' error path is untested",
      "location": "/Users/dave/src/bzr/src/tls/tofu.rs:98-101",
      "description": "After a successful HEAD, probe_server_cert reads capture.captured; if the OnceLock was never set (e.g. a plain-HTTP URL where verify_server_cert never ran, or a connection that completes without invoking the verifier) it returns BzrError::config(\"no certificate captured from {url}\"). This is the only branch distinguishing 'connected but no TLS cert seen' from 'connection failed', and it has no coverage \u2014 an http:// URL would hit it. A regression that, say, returns a default fingerprint instead of erroring would let TOFU pin garbage.",
      "why_uncovered": "tofu_tests.rs::probe_server_cert_returns_error_for_unreachable only covers the send() failure path (unreachable host). The post-send 'captured.get() == None' branch is never reached because no test points the probe at a non-TLS endpoint that connects successfully.",
      "severity": "medium",
      "suggested_test": "Point probe_server_cert at an http:// (non-TLS) wiremock server URL that responds 200 to HEAD; assert the result is Err and the message contains \"no certificate captured\".",
      "subsystem": "tls-keyring",
      "verdict": {
        "id": "tls-keyring-no-cert-captured-error-untested",
        "confirmed": true,
        "reason": "The 'no certificate captured' branch at src/tls/tofu.rs:98-101 fires only when client.head(url).send() succeeds but capture.captured (OnceLock) was never set. The sole probe test, tofu_tests.rs:73-76 (probe_server_cert_returns_error_for_unreachable), targets https://127.0.0.1:1/unreachable, which errors at the send() call (tofu.rs:94-96) \u2014 the connection-failure path, not the post-send captured.get()==None branch. No other unit test calls probe_server_cert against a connecting-but-non-TLS endpoint. The test file's own comment at tofu_tests.rs:130-132 explicitly notes 'the post-handshake success path in probe_server_cert require[s] infrastructure this project doesn't have', confirming it is uncovered. Functional tests (tests/functional/run-tests.sh:21,101) use http:// URLs and never invoke the TLS/TOFU probe (probe_server_cert is only called from shared.rs:129 and config.rs:184 in the HTTPS pinning flow). So the distinguishing 'connected but no TLS cert seen' branch genuinely has no test.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "tls-keyring-stub-not-built-untested-by-default",
      "kind": "coverage-gap",
      "title": "keyring_stub fallback bodies are never compiled or tested in the default/CI build",
      "location": "/Users/dave/src/bzr/src/credentials/keyring_stub.rs:16-29",
      "description": "The stub backend (used when the `keyring` feature is OFF) returns the 'compiled without keyring support' error from store/retrieve/delete and is the path real users get on a no-keyring build. The functions carry #[cfg_attr(test, mutants::skip)] and the file comment explicitly states cargo-mutants runs --all-features so these bodies are never compiled into the test binary; keyring_stub_tests.rs only runs when the keyring feature is disabled. If CI always builds with the keyring feature on, this fallback (and its error message) ships with zero executed test coverage, so a broken message or a stub accidentally returning Ok would not be caught.",
      "why_uncovered": "keyring_stub_tests.rs exists but is gated behind cfg(not(feature=\"keyring\")); the default and mutants (--all-features) builds compile keyring.rs instead, leaving the stub untested in the primary CI matrix.",
      "severity": "low",
      "suggested_test": "Add a CI job (or `cargo test --no-default-features`) that builds with the keyring feature disabled and runs keyring_stub_tests.rs, ensuring the UNSUPPORTED message and Err returns are exercised in CI rather than only locally.",
      "subsystem": "tls-keyring",
      "verdict": {
        "id": "tls-keyring-stub-not-built-untested-by-default",
        "confirmed": true,
        "reason": "Confirmed. src/credentials/mod.rs:11-13 gates keyring_stub.rs behind #[cfg(not(feature = \"keyring\"))]; Cargo.toml:24 sets default = [\"keyring\"]. Every CI test run builds with keyring ON: .github/workflows/ci.yml:27 (MSRV) and :68 (Test) use `cargo test --locked` (default features), and coverage at :168 uses `--all-features` \u2014 all compile keyring.rs, not keyring_stub.rs. The three stub tests in src/credentials/keyring_stub_tests.rs:5-21 (store_returns_unsupported, retrieve_returns_unsupported, delete_returns_unsupported) therefore never compile or run in CI. The only `--no-default-features` invocations (ci.yml:91, release.yml:54) run `cargo run -p xtask` for man-page generation, which executes the xtask binary and does not build or run the bzr library test binary. No CI job runs `cargo test --no-default-features`; the cross matrix (ci.yml:108-130) uses `cross check` (compile-only, default features). So the stub bodies and their tests have zero executed coverage in the primary CI matrix, exactly as claimed. Severity is low rather than medium: the affected code is three trivial one-line `Err(BzrError::Keyring(UNSUPPORTED.into()))` returns, tests do exist (just unexecuted in default CI), and the path only affects --no-default-features builds.",
        "refined_severity": "low"
      }
    },
    {
      "id": "xmlrpc-http-error-body-slice-panic",
      "kind": "likely-bug",
      "title": "HTTP error body truncation can panic on UTF-8 char boundary",
      "location": "src/xmlrpc/client.rs:77",
      "description": "On a 4xx/5xx XML-RPC response the code logs `&body[..body.len().min(512)]`. Slicing a `String` by byte index panics if byte 512 lands inside a multi-byte UTF-8 character. Bugzilla error pages routinely contain non-ASCII (smart quotes, accented names, etc.), so a real-world error response longer than 512 bytes whose 512th byte is mid-codepoint will panic the process instead of returning the intended `BzrError::HttpStatus`. The error path itself is otherwise correct, but the debug-log truncation is unsafe. Should use `body.char_indices()` / `floor_char_boundary` or `body.get(..n)` with a boundary-safe fallback.",
      "why_uncovered": "The only HTTP-error test, `http_error_maps_to_xmlrpc_error` (client_tests.rs:209), returns a 500 with an empty body, so the slice is `[..0]` and never crosses a char boundary. No test sends an error body >512 bytes or containing multi-byte UTF-8.",
      "severity": "low",
      "suggested_test": "Add a wiremock test: respond_with(ResponseTemplate::new(500).set_body_string(<a 600+ byte string whose byte 512 is mid multi-byte char, e.g. 511 ASCII bytes followed by repeated '\u00e9'>)). Assert client.get_bug(\"1\") returns Err(BzrError::HttpStatus{status:500,..}) and does not panic.",
      "subsystem": "xmlrpc",
      "verdict": {
        "id": "xmlrpc-http-error-body-slice-panic",
        "confirmed": true,
        "reason": "Bug is real and uncovered. src/xmlrpc/client.rs:77 logs `body = &body[..body.len().min(512)]`, slicing a String by byte index; when body.len() > 512 and byte 512 lands mid-UTF-8-codepoint this panics instead of returning BzrError::HttpStatus. The only HTTP-error test, `http_error_maps_to_xmlrpc_error` (client_tests.rs:209-222), uses `ResponseTemplate::new(500)` with no body, so body.len()==0 and the slice is [..0] \u2014 never crossing a char boundary. Grep of src/xmlrpc/client_tests.rs shows no other 4xx/5xx response and no error body containing multi-byte UTF-8; functional tests (tests/functional/run-tests.sh) exercise only success-path xmlrpc commands. Caveat lowering severity: the slice is inside `tracing::debug!`, whose field expression is only evaluated when debug logging is enabled (`-vv` or RUST_LOG=debug). At default verbosity the event is disabled and the slice is not evaluated, so the panic requires debug logging plus a non-ASCII error body longer than 512 bytes crossing byte 512 \u2014 a real but conditionally-reachable panic.",
        "refined_severity": "low"
      }
    },
    {
      "id": "xmlrpc-empty-element-events-ignored",
      "kind": "likely-bug",
      "title": "Self-closing XML elements (Event::Empty) are silently ignored, causing EOF error",
      "location": "src/xmlrpc/parsing.rs:108-169",
      "description": "Every parsing loop matches only `Event::Start` / `Event::End` / `Event::Text` and falls through `_ => {}` for `Event::Empty`. quick_xml emits `Event::Empty` (not Start+End) for self-closing tags. A response containing `<value/>`, `<boolean/>`, `<array/>`, `<struct/>`, `<data/>`, or an empty `<member/>` will never be matched: the loop keeps reading until EOF and returns `BzrError::XmlRpc(\"unexpected EOF ...\")` instead of the intended empty value/string/array. This affects parse_value_content (empty value -> should be empty string), parse_array (`<data/>` -> empty array), parse_struct, and read_text_content. Real Bugzilla XML-RPC serializers commonly emit `<value/>` or empty typed elements for empty/null fields.",
      "why_uncovered": "`parse_empty_value_returns_empty_string` (parsing_tests.rs:236) and `parse_empty_array` (parsing_tests.rs:144) only use the separate-tag form `<value></value>` / `<data></data>`, which produces Start then End events and is handled. No test feeds the self-closing `<value/>` / `<data/>` / `<array/>` forms.",
      "severity": "medium",
      "suggested_test": "Add parse tests feeding `<methodResponse><params><param><value/></param></params></methodResponse>` (expect empty string) and `<...><value><array><data/></array></value>...` and `<value><struct/></value>` (expect empty array/struct). Each currently fails with an unexpected-EOF error.",
      "subsystem": "xmlrpc",
      "verdict": {
        "id": "xmlrpc-empty-element-events-ignored",
        "confirmed": true,
        "reason": "Empirically verified by feeding self-closing forms through parse_response (temporary probe test, since reverted). Results: `<value/>` -> Err(XmlRpc(\"unexpected EOF looking for value\")); `<struct/>` -> Ok(String(\"\")) (should be empty struct); `<array/>` -> Ok(String(\"\")) (should be empty array). `<data/>` happens to return Ok(Array([])) and is fine. Root cause confirmed in src/xmlrpc/parsing.rs: every loop (parse_value lines 87-92, parse_value_content 109-169, parse_array 199-218, parse_struct 228-239, read_text_content 175-191) matches only Event::Start/End/Text and falls through `_ => {}` for Event::Empty. quick-xml 0.40 (Cargo.toml:56) is never configured with expand_empty_elements, which defaults to false, so self-closing tags emit Event::Empty (not Start+End). No test covers self-closing forms: parse_empty_value_returns_empty_string (parsing_tests.rs:236), parse_empty_array (parsing_tests.rs:144), parse_array_without_data_tag_is_empty (parsing_tests.rs:244), and parse_empty_struct (parsing_tests.rs:128) all use the separate open/close-tag form (<value></value>, <data></data>) which produces Start+End and is handled. The claimed <value/> EOF case is real; <struct/>/<array/> additionally return the wrong type. The exact description of <data/> is inaccurate (it works), but the core gap is genuine and uncovered.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "xmlrpc-empty-field-list-blank-token",
      "kind": "coverage-gap",
      "title": "include_fields/exclude_fields with empty or blank value produces array with empty string element",
      "location": "src/xmlrpc/client.rs:268-279",
      "description": "add_field_lists splits the CSV on ',' unconditionally: `fields.split(',').map(|f| Value::from(f.trim()))`. For `include_fields: Some(\"\")` this yields `[Value::String(\"\")]` \u2014 a one-element array containing an empty field name sent to Bugzilla, rather than omitting include_fields. For `Some(\"id,,summary\")` (trailing/double commas) it yields an empty-string element between valid names. Bugzilla may reject or mis-handle an empty include/exclude field token, and the request differs from the REST path. No filtering of empty tokens is done.",
      "why_uncovered": "search_bugs_fields_and_ids_use_xmlrpc_arrays (client_tests.rs:273) only exercises well-formed `\"id, summary\"` and `\"cc\"`. No test passes an empty string, a leading/trailing comma, or a doubled comma to include_fields/exclude_fields.",
      "severity": "low",
      "suggested_test": "Unit-test add_field_lists with SearchParams{include_fields:Some(\"\".into()),..} and Some(\"id,,summary\".into()): assert the resulting Value::Array contains no empty-string elements (i.e. empty tokens are filtered out).",
      "subsystem": "xmlrpc",
      "verdict": {
        "id": "xmlrpc-empty-field-list-blank-token",
        "confirmed": true,
        "reason": "Code at src/xmlrpc/client.rs:274-276 splits the CSV on ',' with no empty-token filtering: fields.split(',').map(|f| Value::from(f.trim())). Some(\"\") yields a one-element array [Value::String(\"\")]; Some(\"id,,summary\") yields a middle empty-string element. The only test exercising add_field_lists is search_bugs_fields_and_ids_use_xmlrpc_arrays (src/xmlrpc/client_tests.rs:273), which uses well-formed include_fields: Some(\"id, summary\") (line 295) and exclude_fields: Some(\"cc\") (line 296). No xmlrpc test passes an empty string, leading/trailing comma, or doubled comma (grep of client_tests.rs found only \"id, summary\"/\"cc\"; the String::new() matches at lines 496/510 are for unrelated get_nonempty_str/get_datetime_str helpers). tests/functional/run-tests.sh has zero references to include_fields/exclude_fields. The fields come from Option<String> clap args (src/cli/bug.rs:102, src/cli/query.rs:93 etc.), so --include-fields \"\" is reachable. Gap is genuinely uncovered. Severity low: requires deliberately malformed input, no crash/corruption, and the REST CSV-passthrough path carries the same token content, so it is a minor edge-case divergence rather than a correctness failure on normal use.",
        "refined_severity": "low"
      }
    },
    {
      "id": "xmlrpc-extract-attachments-wrong-shape-error",
      "kind": "coverage-gap",
      "title": "No test for malformed-shape error paths in extract_attachments / extract_comments / get_group",
      "location": "src/xmlrpc/client.rs:419-433, 362-384, 166-184",
      "description": "extract_attachments expects the per-bug entry to be a `Value::Array` (line 424) and errors with 'expected attachments array' otherwise; extract_comments expects the entry to be a struct (line 367-369) and the comments member to be an array (line 375-377); get_group expects top to be a struct and a 'groups' array (lines 172-178). These ok_or_else error branches map malformed/unexpected response shapes to specific BzrError::XmlRpc messages. None of these specific error messages are asserted by any unit or functional test \u2014 only the happy-path and empty-result shapes are covered. A regression that swallows or mis-maps these (e.g. returning Ok(empty) instead of Err) would not be caught.",
      "why_uncovered": "client_tests.rs covers get_attachments full/empty (lines 643,850), get_comments_since full/empty (563,615), and value_to_group_info directly (358) \u2014 all well-formed. No test feeds, e.g., a bug entry that is a struct where an array is expected, or a Group.get response missing the 'groups' array, to assert the XmlRpc error is produced.",
      "severity": "medium",
      "suggested_test": "Add wiremock/unit tests: (1) Bug.attachments response where bugs->{id} is a <struct> instead of <array> -> assert Err containing 'expected attachments array'; (2) Group.get response with no 'groups' member -> assert Err 'expected groups array'; (3) Bug.comments where the comments member is a <string> -> assert Err 'expected comments array'.",
      "subsystem": "xmlrpc",
      "verdict": {
        "id": "xmlrpc-extract-attachments-wrong-shape-error",
        "confirmed": true,
        "reason": "Real coverage gap. The four malformed-shape error messages \u2014 \"expected attachments array\" (src/xmlrpc/client.rs:426), \"expected bug entry struct\" (369), \"expected comments array\" (377), and get_group's \"expected struct response\"/\"expected groups array\" (174/178) \u2014 have ZERO assertions in src/xmlrpc/client_tests.rs (grep returns no matches). The functions extract_attachments and extract_comments are not even imported into the test module (client_tests.rs:5-8 imports only extract_bugs, extract_id, value_to_comment, value_to_group_info). Existing tests cover only well-formed shapes: xmlrpc_get_attachments_parses_full_response (line 644/685), xmlrpc_get_attachments_returns_empty_when_bug_has_none (850/861), xmlrpc_get_comments_since_parses_full_response (563/604), and empty/serialize variants. value_to_group_info tests (359, 385) feed a well-formed Value::Struct directly, bypassing get_group's shape checks. The only \"expected struct response\" assertion (client_tests.rs:344) is in extract_id_requires_struct_with_integer_id \u2014 a different function (extract_id), not get_group. Functional tests (tests/functional/run-tests.sh:301-357) exercise group/attachment/comment commands only against a real Bugzilla server (happy paths), never feeding malformed XML-RPC shapes. A regression returning Ok(empty) instead of Err on a struct-where-array-expected response would not be caught.",
        "refined_severity": "medium"
      }
    },
    {
      "id": "xmlrpc-int-overflow-non-i64",
      "kind": "coverage-gap",
      "title": "Integer values exceeding i64 range surface as parse error with no coverage",
      "location": "src/xmlrpc/parsing.rs:116-122",
      "description": "`<int>`/`<i4>` content is parsed with `text.parse::<i64>()`. An out-of-range integer (e.g. a 20-digit number, or a value > i64::MAX that Bugzilla could emit for large IDs/sizes) returns BzrError::XmlRpc(\"invalid integer ...\"). Likewise `<double>` parse failure (parsing.rs:129) and `<base64>` decode failure (parsing.rs:143) are error paths. The invalid-integer / invalid-double / invalid-base64 branches have no test asserting the error message or that the whole response parse fails cleanly rather than panicking.",
      "why_uncovered": "parsing_tests.rs covers valid int (parse_i4_type:160), valid double (parse_response_with_double_and_datetime:81), and valid base64 (parse_base64_value:203). No test feeds malformed numeric/base64 content (e.g. <int>99999999999999999999</int>, <double>abc</double>, <base64>!!!</base64>) to confirm the mapped XmlRpc error.",
      "severity": "low",
      "suggested_test": "Add parse_response tests with <int>99999999999999999999</int>, <double>not-a-number</double>, and <base64>@@@invalid@@@</base64>; assert each returns Err(BzrError::XmlRpc(_)) with the expected 'invalid integer/double/base64' substring.",
      "subsystem": "xmlrpc",
      "verdict": {
        "id": "xmlrpc-int-overflow-non-i64",
        "confirmed": true,
        "reason": "Verified against src/xmlrpc/parsing_tests.rs (the sibling test file for parsing.rs). The three error branches at parsing.rs:118-120 (int/i4 parse, incl. i64 overflow), 129-131 (double parse), and 143 (base64 decode) have no test feeding malformed content. The tests only cover VALID values: parse_i4_type (parsing_tests.rs:160-165, <i4>42</i4>), int 12345/102 in parse_success_response_with_struct:24 and parse_fault_response:60, parse_response_with_double_and_datetime:81-90 (<double>42.5</double>), and parse_base64_value:203-208 (<base64>SGVsbG8=</base64>). The error-path tests that exist target unrelated branches: parse_empty_params_errors_with_specific_message:253 and parse_fault_response:52. A repo-wide grep for \"invalid integer\"/\"invalid double\"/\"invalid base64\"/overflow/i64::MAX in src/ and tests/ found only production code (parsing.rs:118-143) and unrelated TLS tests \u2014 no parsing test asserting these XmlRpc error mappings. Functional tests (run-tests.sh:1117,1324,1347) exercise --api xmlrpc only against real Bugzilla emitting valid data, so they never hit these branches. The gap is real. Severity is low: the code uses safe .parse()/.decode() with .map_err(...)? (no panic risk masked \u2014 worst case is an untested-but-correct error message), and the i64-overflow-for-large-IDs scenario is largely speculative since Bugzilla IDs/sizes fit i64; the legitimate value is malformed-input robustness coverage.",
        "refined_severity": "low"
      }
    }
  ],
  "sequences": [
    {
      "title": "Product->component->bug->resolve->reopen full lifecycle with state verification at each transition",
      "rationale": "The existing suite creates a product/component/bug and resolves separately, but never walks a single bug through NEW -> CONFIRMED -> RESOLVED FIXED -> REOPENED while re-querying after each step to confirm the prior write actually landed before the next mutation. State-machine ordering bugs (e.g. a status transition silently rejected by the server but reported as success per finding client-bug-update-200-error-swallowed) only surface when each step asserts the cumulative state, not just exit 0 of the mutation. Also exercises the component default-assignee -> bug.assigned_to roundtrip which is never verified.",
      "steps": [
        "bzr product create --name XRefProd --description 'cross-ref lifecycle product' -> assert exit 0 (or idempotent 'already exists'); capture .id",
        "bzr component create --product XRefProd --name Core --description 'core' --default-assignee admin@test.bzr -> assert exit 0 or idempotent",
        "bzr bug create --product XRefProd --component Core --summary 'lifecycle bug' --description 'init' --op-sys All --rep-platform All -> assert exit 0, capture .id as LB",
        "bzr bug view $LB --json -> assert .assigned_to == 'admin@test.bzr' (proves component default-assignee propagated into the created bug; never asserted today)",
        "bzr bug update $LB --status CONFIRMED -> assert exit 0",
        "bzr bug view $LB --json -> assert .status == 'CONFIRMED' (confirms the transition persisted before next step)",
        "bzr bug update $LB --status RESOLVED --resolution FIXED --comment 'fixed in lifecycle test' -> assert exit 0",
        "bzr bug view $LB --json -> assert .status == 'RESOLVED' AND .resolution == 'FIXED'",
        "bzr comment list $LB --json -> assert last comment .text contains 'fixed in lifecycle test' (proves the atomic comment landed alongside the status change, not just one or the other)",
        "bzr bug update $LB --status REOPENED --resolution '' -> assert exit 0",
        "bzr bug view $LB --json -> assert .status == 'REOPENED' AND .resolution == '' (proves resolution was cleared on reopen, a state-clearing transition never tested)"
      ],
      "targets": [
        "client-bug-update-200-error-swallowed",
        "component default-assignee -> bug.assigned_to roundtrip (untested)",
        "status REOPENED transition + resolution clearing (untested transition)"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Group create -> add-user -> bug --groups-add -> view roundtrip, then remove-user and re-verify group membership independence",
      "rationale": "The audit notes group membership and bug group-permission interaction is entirely untested as a cross-resource flow: the suite adds a user to a group and separately runs bug --groups-add, but never puts a bug INTO a group and re-queries to confirm the group landed on the bug, nor checks that removing a user from the group does not retroactively alter the bug's group set. This is a genuine cross-resource state dependency (group existence gates whether --groups-add succeeds) that single-command cases cannot express.",
      "steps": [
        "bzr group create --name xref-grp --description 'cross-ref group' -> assert exit 0 or idempotent 'already exists'",
        "bzr group add-user --group xref-grp --user testuser@test.bzr -> assert exit 0",
        "bzr group list-users --group xref-grp --json -> assert at least one entry has .email == 'testuser@test.bzr' (membership precondition established)",
        "bzr bug create --product FuncTestProd --component Backend --summary 'group-gated bug' --description 'g' --op-sys All --rep-platform All -> assert exit 0, capture .id as GB",
        "bzr bug update $GB --groups-add xref-grp -> assert exit 0 (group must exist for this to succeed; ties bug mutation to prior group creation)",
        "bzr bug view $GB --json -> assert .groups contains 'xref-grp' (roundtrip: the group landed on the bug; never verified today)",
        "bzr group remove-user --group xref-grp --user testuser@test.bzr -> assert exit 0",
        "bzr bug view $GB --json -> assert .groups still contains 'xref-grp' (removing a USER from the group must NOT strip the GROUP from the bug; verifies the two relationships are independent)",
        "bzr bug update $GB --groups-remove xref-grp -> assert exit 0",
        "bzr bug view $GB --json -> assert .groups does NOT contain 'xref-grp'"
      ],
      "targets": [
        "group membership <-> bug group-permission interaction (untested cross-resource flow)",
        "bug --groups-add/--groups-remove roundtrip verification (mutation asserted but never re-queried)"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Clone roundtrip preserving relationships: --add-depends-on actually links, and clone copies description from comment #0",
      "rationale": "Findings commands-bug-clone-missing-comment-zero and the audit gap 'Bug clone: does not verify relationship (--add-depends-on) actually created link' both flag that clone mutations are asserted only for exit 0 and a returned .id, never re-queried. A clone is a multi-step server operation (create bug, copy description from source comment #0, optionally POST a dependency); if the description is silently lost or the dependency link is never created, current tests pass. This sequence carries the source bug id forward and asserts the clone's actual stored description and dependency edge.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'clone-src with body' --description 'UNIQUE-CLONE-BODY-MARKER-7731' --priority High --severity major --op-sys Linux --rep-platform PC -> assert exit 0, capture .id as SRC",
        "bzr bug clone $SRC --add-depends-on --op-sys Linux --rep-platform PC -> assert exit 0, capture .id as CL",
        "bzr bug view $CL --json -> assert .summary == 'clone-src with body' AND .priority == 'High' (clone copied scalar fields)",
        "bzr comment list $CL --json -> assert comment with .count == 0 .text contains 'UNIQUE-CLONE-BODY-MARKER-7731' (proves the original description was carried into the clone's comment #0; the silent-description-loss branch in clone.rs would fail here)",
        "bzr bug view $CL --json -> assert .depends_on array contains $SRC (proves --add-depends-on created the real edge, not just a success message)",
        "bzr bug view $SRC --json -> assert .blocks array contains $CL (the reciprocal edge must exist on the source side; cross-resource consistency of the dependency relationship)"
      ],
      "targets": [
        "commands-bug-clone-missing-comment-zero",
        "commands-bug-clone-comment-failure-after-create",
        "Bug clone --add-depends-on relationship roundtrip (audit gap)"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Dupe-of transition propagates into dependent bug's dependency views and CC, then verify reverse navigation",
      "rationale": "The existing dupe test (34c/34d) only checks the source bug's own status/resolution/dupe_of. It never checks the cross-resource consequence: marking a bug DUPLICATE adds the reporter to the target's CC and the relationship should be navigable from the target. This sequence builds a dependency edge first, then dupes the source, and asserts the target reflects the new relationship state - an ordering-sensitive interaction (edge must exist before dupe) that single-command cases miss.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'dupe-target X' --description 't' --op-sys All --rep-platform All -> assert exit 0, capture .id as TGT",
        "bzr bug create --product FuncTestProd --component Backend --summary 'dupe-source X' --description 's' --op-sys All --rep-platform All -> assert exit 0, capture .id as SRC",
        "bzr bug update $SRC --depends-on-add $TGT -> assert exit 0 (establish a relationship BEFORE the dupe transition)",
        "bzr bug view $SRC --json -> assert .depends_on contains $TGT",
        "bzr bug update $SRC --dupe-of $TGT -> assert exit 0",
        "bzr bug view $SRC --json -> assert .status == 'RESOLVED' AND .resolution == 'DUPLICATE' AND .dupe_of == $TGT",
        "bzr bug view $TGT --json -> assert .see_also OR .blocks reflects the duped source where applicable; minimally assert .id == $TGT still resolves and the prior depends_on edge from SRC is still visible as .blocks contains $SRC (relationship survives the dupe transition)",
        "bzr comment list $TGT --json -> assert array length >= 1 (target remains viewable; sanity that duping the source did not corrupt target comment retrieval)"
      ],
      "targets": [
        "dupe-of transition reflected in later cross-resource views (audit focus item)",
        "relationship persistence across a status-transition mutation"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "bug update with no mutating flags is a silent no-op PUT; and --deadline garbage should fail client-side but currently reaches the server",
      "rationale": "Two confirmed likely-bugs interact here. cli-parsing-bug-update-no-fields-empty-put: 'bzr bug update <id>' with zero change flags builds an empty UpdateBugParams and PUTs it, printing success. validation-parsing-deadline-unvalidated: --deadline accepts arbitrary strings with no exit-7 validation (unlike every other date flag). A state-carrying sequence captures last_change_time before and after to prove the no-op actually mutated nothing, and asserts the malformed-deadline exit code, exposing the inconsistency with --changed-since (test 45b asserts exit 7).",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'noop-update bug' --description 'n' --op-sys All --rep-platform All -> assert exit 0, capture .id as NB",
        "bzr bug view $NB --json -> capture .last_change_time as T0",
        "bzr bug update $NB -> CURRENTLY asserts exit 0 with 'Updated bug' message; assert exit 0 to document current behavior (the desired behavior is a validation error - flag this divergence)",
        "bzr bug view $NB --json -> assert .last_change_time == T0 (proves the empty PUT was a true no-op; if the server bumped the timestamp on an empty body this would catch it)",
        "bzr bug update $NB --deadline 2026-12-31 -> assert exit 0",
        "bzr bug view $NB --json -> assert .deadline == '2026-12-31' (valid deadline path still works)",
        "bzr bug update $NB --deadline not-a-date -> assert exit 7 is the DESIRED outcome; document actual exit code. If exit != 7, this confirms validation-parsing-deadline-unvalidated (garbage shipped to server)",
        "bzr bug view $NB --json -> assert .deadline == '2026-12-31' (the garbage update must not have overwritten the previously-valid deadline)"
      ],
      "targets": [
        "cli-parsing-bug-update-no-fields-empty-put",
        "validation-parsing-deadline-unvalidated"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "query save mixing --search with structured filters, then run, exposes silent filter loss",
      "rationale": "Finding cli-parsing-query-save-search-filter-no-conflict: clap accepts 'query save name --search foo --product Bar --status NEW' even though docs claim mutual exclusivity; handle_save sets kind=Search and stores the structured filters but run dispatches on quicksearch only, silently ignoring the product/status. This is only observable across save -> show -> run: the saved definition contains contradictory state, and running it returns search-driven results that ignore the filters. No single-command test can reveal the stored-vs-executed divergence.",
      "steps": [
        "bzr query save mixed-q --search 'Bug one' --product FuncTestProd --status NEW --limit 5 -> CURRENTLY succeeds (assert exit 0); the DESIRED behavior is a clap conflict error like --from-url has",
        "bzr query show mixed-q --json -> assert .kind == 'search' AND .search contains 'Bug one'; then assert whether .product/.status were also persisted (if present, confirms the bug: contradictory stored state)",
        "bzr query run mixed-q --json -> assert exit 0; results are quicksearch-driven. Assert that results are NOT constrained by --product/--status (e.g. a NEW-only product filter is ignored) by checking returned bugs include a non-NEW or non-FuncTestProd match if quicksearch alone would return one",
        "bzr query delete mixed-q -> assert exit 0 (cleanup)",
        "bzr query show mixed-q -> assert failure (deleted)"
      ],
      "targets": [
        "cli-parsing-query-save-search-filter-no-conflict"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Template captures component default-assignee chain: create-from-template inherits assignee, then template survives source component update",
      "rationale": "Templates are local state that reference server-side product/component names. No test exercises the interaction where a template is created, then the underlying component's default-assignee is changed server-side, then a bug created from the template - to confirm the template's stored fields vs. the live component default are resolved correctly. This is a cross-layer (local config + server) state dependency that single commands cannot express.",
      "steps": [
        "bzr component create --product FuncTestProd --name TmplComp --description 'tmpl component' --default-assignee admin@test.bzr -> assert exit 0 or idempotent",
        "bzr template save chain-tmpl --product FuncTestProd --component TmplComp --priority Normal --severity normal -> assert exit 0",
        "bzr template show chain-tmpl --json -> assert .component == 'TmplComp' AND .product == 'FuncTestProd'",
        "bzr bug create --template chain-tmpl --summary 'from chain-tmpl A' --description 'a' --op-sys All --rep-platform All -> assert exit 0, capture .id as TB1",
        "bzr bug view $TB1 --json -> assert .component == 'TmplComp' AND .priority == 'Normal' AND .assigned_to == 'admin@test.bzr' (template product/component drove the component's default assignee)",
        "bzr bug create --template chain-tmpl --summary 'from chain-tmpl B' --priority Highest --description 'b' --op-sys All --rep-platform All -> assert exit 0, capture .id as TB2 (CLI flag overrides template field)",
        "bzr bug view $TB2 --json -> assert .priority == 'Highest' AND .component == 'TmplComp' (proves CLI flag overrode the template's Normal while inheriting the rest)",
        "bzr template delete chain-tmpl -> assert exit 0"
      ],
      "targets": [
        "template -> bug create field inheritance + component default-assignee resolution (cross-layer, untested)",
        "template CLI-flag-over-template-field precedence (untested with verification)"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Attachment obsolete state transition reflected in subsequent list, plus atomic attachment+comment ordering verified by count and content",
      "rationale": "Existing tests upload, list, update --obsolete true, and check --comment count growth, but never re-query the list AFTER marking obsolete to confirm is_obsolete flipped, nor verify the atomic attachment+comment pair carries the right comment text tied to the right attachment across the POST->GET->PUT sequence. Marking obsolete is a state transition whose persistence is only observable in a later list call.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'attach-state bug' --description 'a' --op-sys All --rep-platform All -> assert exit 0, capture .id as AB",
        "printf 'obsolete-state-marker' > /tmp/bzr-xref-att.txt then bzr attachment upload $AB /tmp/bzr-xref-att.txt --summary 'will-obsolete' --comment 'ATTACH-COMMENT-MARKER' -> assert exit 0, capture .id as AID",
        "bzr comment list $AB --json -> assert a comment with .attachment_id == AID has .text containing 'ATTACH-COMMENT-MARKER' (proves atomic attachment+comment tied the comment to THIS attachment, not a stray one)",
        "bzr attachment list $AB --json -> assert the entry with .id == AID has .is_obsolete == false",
        "bzr attachment update $AID --obsolete true -> assert exit 0",
        "bzr attachment list $AB --json -> assert the entry with .id == AID now has .is_obsolete == true (state transition persisted and visible in the later list - never re-queried today)",
        "bzr attachment update $AID --summary 'renamed-after-obsolete' -> assert exit 0",
        "bzr attachment list $AB --json -> assert .id == AID entry has .summary == 'renamed-after-obsolete' AND .is_obsolete == true (later mutation did not reset the obsolete flag)"
      ],
      "targets": [
        "attachment --obsolete transition reflected in later list (re-query gap)",
        "atomic attachment+comment association by attachment_id (POST->GET->PUT correctness)"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "bug my --created --cc combination skips the assigned-to search, observable only by comparing result sets across runs",
      "rationale": "Finding commands-bug-my-created-and-cc-combo: --created and --cc are not mutually exclusive, and when both are set (without --all) the assigned-to search is skipped (my.rs line 46). The surprising semantics (assigned bugs excluded) are only detectable by setting up a bug that is assigned-to-me but neither created-by-me nor cc-me, then confirming it appears in 'bug my' (assigned) but disappears in 'bug my --created --cc'. Pure cross-resource state setup that no single command covers.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'assigned-not-created bug' --description 'x' --op-sys All --rep-platform All -> assert exit 0, capture .id as MB (created by admin, the test user)",
        "bzr bug update $MB --assignee admin@test.bzr --reset-qa-contact -> assert exit 0 (ensure assigned to the authenticated user)",
        "bzr bug my --json -> assert results array contains an entry with .id == MB (default = assigned-to-me includes it)",
        "bzr bug my --created --json -> assert contains .id == MB (admin created it too)",
        "bzr bug my --created --cc --json -> assert exit 0; this runs creator+cc searches but SKIPS the assigned search. Document the result. Compare against 'bug my --all' which includes assigned. The semantic difference (--created --cc != --all) is the target",
        "bzr bug my --all --json -> assert contains .id == MB (the all path always includes assigned)"
      ],
      "targets": [
        "commands-bug-my-created-and-cc-combo"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Cross-API consistency: create/mutate via REST, then read back via XML-RPC and hybrid to confirm the same bug state across transports",
      "rationale": "The audit notes there is no matrix REST/XML-RPC testing for most commands - only comments/attachments. A bug mutated through REST should be readable with identical core state through XML-RPC and hybrid. This catches transport-specific deserialization drift (e.g. the force_id_fields normalization differing on the XML-RPC fallback leg per client-bug-hybrid-search-id-fallback-not-tested) and confirms the same write is observed identically regardless of read transport - a cross-mode state dependency single-mode tests miss.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'multimode bug' --description 'm' --priority High --op-sys All --rep-platform All -> assert exit 0, capture .id as XB",
        "bzr bug update $XB --whiteboard 'multimode-marker' --status CONFIRMED -> assert exit 0",
        "bzr --api rest bug view $XB --json -> assert .whiteboard == 'multimode-marker' AND .status == 'CONFIRMED' AND .priority == 'High'; capture .summary as S_REST",
        "bzr --api hybrid bug view $XB --json -> assert .whiteboard == 'multimode-marker' AND .summary == S_REST (hybrid read agrees with REST read)",
        "bzr --api hybrid bug search --query 'multimode-marker' --fields summary,status --limit 10 --json -> assert at least one result has .id == XB (id-less --fields must still carry id through the hybrid path so the result is identifiable; targets the untested id-injection-on-fallback leg)",
        "bzr --api xmlrpc comment list $XB --json -> assert comment .count == 0 .text contains 'm' (the create description is readable over XML-RPC; cross-transport read of the description written via REST)"
      ],
      "targets": [
        "client-bug-hybrid-search-id-fallback-not-tested",
        "cross-transport REST/XMLRPC/hybrid read consistency for a single mutated bug (matrix gap)"
      ],
      "api_modes": [
        "rest",
        "hybrid",
        "xmlrpc"
      ]
    },
    {
      "title": "Cross-mode read consistency for the SAME bug across rest/xmlrpc/hybrid (field parity + id deserialization)",
      "rationale": "Every existing bug-view case uses the default (auto/rest) mode. No case fetches the SAME bug through all three transports back-to-back and asserts the returned object is field-identical. XML-RPC ignores include_fields (force_id_fields is a no-op there) while REST honors it, so a divergence in id/status/priority/summary between transports is invisible to single-mode tests. This catches the hybrid id-injection (force_id_fields) and XML-RPC parsing (Event::Empty / int overflow) producing a structurally different object than REST for the same data \u2014 a state-free single command can never compare two transports' output of one resource.",
      "steps": [
        "bzr config set-server test --url $BZ_URL --api-key $API_KEY --auth-method query_param --email $ADMIN_EMAIL : assert exit 0",
        "bzr bug create --product FuncTestProd --component Backend --summary 'Matrix parity bug' --description 'desc' --priority High --severity normal --op-sys Linux --rep-platform PC : assert exit 0, capture MBUG=.id",
        "bzr --api rest bug view $MBUG --fields id,summary,status,priority : assert exit 0, capture REST_JSON; assert .id==$MBUG, .summary=='Matrix parity bug', .priority=='High'",
        "bzr --api xmlrpc bug view $MBUG --fields id,summary,status,priority : assert exit 0; assert .id==$MBUG (id survives even though XML-RPC ignores --fields), .summary=='Matrix parity bug', .priority=='High'",
        "bzr --api hybrid bug view $MBUG --fields id,summary,status,priority : assert exit 0; assert .id==$MBUG, .summary and .priority equal the REST values captured in step 3 (parity assertion: jq-extracted summary/priority match across all three modes)"
      ],
      "targets": [
        "client-bug-hybrid-search-id-fallback-not-tested",
        "xmlrpc-empty-element-events-ignored",
        "xmlrpc-int-overflow-non-i64",
        "client-bug-get-bug-hybrid-empty-search-fallback"
      ],
      "api_modes": [
        "rest",
        "xmlrpc",
        "hybrid"
      ]
    },
    {
      "title": "Private comment count parity: rest UNDER-reports, xmlrpc/hybrid report full set for the same bug",
      "rationale": "Cases 94b/94c assert hybrid and xmlrpc each return the private comment, but NO case asserts that plain --api rest returns FEWER comments than hybrid/xmlrpc for the same bug \u2014 i.e. that the fallback is actually doing something. Without the rest baseline captured in the same sequence, a regression where hybrid silently stopped falling back (and rest happened to return private data too, or hybrid returned the rest-filtered set) would pass 94b/94c. This sequence pins the differential: rest_count < hybrid_count == xmlrpc_count, which only a state-carrying multi-step comparison can assert.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Priv comment matrix' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture PBUG=.id",
        "bzr comment add $PBUG --body 'public one' : assert exit 0",
        "bzr comment add $PBUG --body 'secret one' --private : assert exit 0",
        "bzr --api rest comment list $PBUG : assert exit 0; capture REST_N = (jq length); capture REST_PRIV = count of is_private==true",
        "bzr --api hybrid comment list $PBUG : assert exit 0; capture HYB_N; assert at least one is_private==true; assert HYB_N >= REST_N",
        "bzr --api xmlrpc comment list $PBUG : assert exit 0; capture XR_N; assert XR_N == HYB_N (xmlrpc and hybrid agree on full set); assert that if REST_PRIV==0 then HYB_N > REST_N (fallback demonstrably added the private comment)"
      ],
      "targets": [
        "client-comment-attachment-download-empty-data-string",
        "xmlrpc-extract-attachments-wrong-shape-error",
        "xmlrpc-empty-element-events-ignored"
      ],
      "api_modes": [
        "rest",
        "hybrid",
        "xmlrpc"
      ]
    },
    {
      "title": "Private attachment download empty-data silent truncation across rest vs hybrid vs xmlrpc",
      "rationale": "Cases 100d/100e download a private attachment via hybrid and xmlrpc and assert content. NO case downloads the SAME private attachment via plain --api rest and checks what happens when REST filters it (the finding: data:'' yields a silent 0-byte file reported as success). This sequence uploads a private attachment, then downloads it in all three modes to DIFFERENT files and compares byte sizes: hybrid/xmlrpc must match the source size; rest must either error or \u2014 if it writes a 0-byte file \u2014 that divergence is now caught by a size assertion. Single-command cases never compare the on-disk result of the same attachment across modes.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Priv attach matrix' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture ABUG=.id",
        "write /tmp/bzr-matrix-attach.txt with known content 'PRIVATE-PAYLOAD-MARKER-12345' (record SRC_SIZE = byte length)",
        "bzr attachment upload $ABUG /tmp/bzr-matrix-attach.txt --summary 'priv matrix' --private : assert exit 0, capture PAID=.id",
        "bzr --api hybrid attachment download $PAID --out /tmp/m-hybrid.txt : assert exit 0; assert file size == SRC_SIZE; assert file contains 'PRIVATE-PAYLOAD-MARKER-12345'",
        "bzr --api xmlrpc attachment download $PAID --out /tmp/m-xmlrpc.txt : assert exit 0; assert file size == SRC_SIZE; assert content equals hybrid file (cmp)",
        "bzr --api rest attachment download $PAID --out /tmp/m-rest.txt : capture exit; assert EITHER exit != 0 (rest refuses) OR (file exists AND size == SRC_SIZE) \u2014 a 0-byte success file fails this assertion, exposing the silent-truncation footgun"
      ],
      "targets": [
        "client-comment-attachment-download-empty-data-string",
        "commands-misc-attachment-download-path-traversal"
      ],
      "api_modes": [
        "hybrid",
        "xmlrpc",
        "rest"
      ]
    },
    {
      "title": "server info parity and partial-failure surface across rest/xmlrpc/hybrid",
      "rationale": "Existing server-info cases assert .version exists per mode but never assert the version STRING is identical across rest and xmlrpc, nor that extensions list parity holds. server_info() makes two sequential REST calls (version then extensions) with no test for divergence; XML-RPC routes differently. This sequence pins that the same server reports the same version through every transport, and that the extensions array length is mode-stable. A regression in version.rs version_to_api_mode classification or the extensions second-call would surface as a cross-mode mismatch invisible to per-mode existence checks.",
      "steps": [
        "bzr --api rest server info : assert exit 0; capture RVER=.version, capture REXT_N = (.extensions | length)",
        "bzr --api xmlrpc server info : assert exit 0; capture XVER=.version; assert XVER == RVER (same server, same version string across transports)",
        "bzr --api hybrid server info : assert exit 0; assert .version == RVER",
        "bzr --api rest server info : re-run and assert (.extensions | length) == REXT_N (extensions call stable, not intermittently empty)"
      ],
      "targets": [
        "client-misc-server-info-partial-failure",
        "client-auth-version-nonnumeric-version"
      ],
      "api_modes": [
        "rest",
        "xmlrpc",
        "hybrid"
      ]
    },
    {
      "title": "auto-detect persistence: --server auto resolves a mode, and a subsequent plain command on that server behaves identically",
      "rationale": "Case 8a runs --server auto whoami once. No sequence exercises auto-detection THEN reuses the same auto server for a real data operation (bug view) and asserts the auto-resolved path returns the same bug as an explicit-mode call. Auth detection (whoami header vs query_param probe) and version fallback run during connect; this sequence carries the auto server config across a whoami + a bug read + a private-comment read, asserting the auto path never under-returns versus explicit hybrid. Catches auth-detection / version-fallback state that only manifests when the detected mode is actually used for a follow-on operation.",
      "steps": [
        "bzr config set-server auto --url $BZ_URL --api-key $API_KEY --email $ADMIN_EMAIL : assert exit 0 (no explicit auth-method, forces detection)",
        "bzr --server auto whoami : assert exit 0, assert .id exists; capture WHO_ID=.id",
        "bzr bug create --product FuncTestProd --component Backend --summary 'Auto mode bug' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture AUBUG=.id (created on default server)",
        "bzr --server auto bug view $AUBUG : assert exit 0; assert .summary=='Auto mode bug', .id==$AUBUG (auto-resolved mode reads the bug correctly)",
        "bzr comment add $AUBUG --body 'auto secret' --private : assert exit 0",
        "bzr --server auto comment list $AUBUG : capture AUTO_N; bzr --api hybrid comment list $AUBUG : capture HYB_N; assert AUTO_N <= HYB_N AND assert AUTO_N >= (number of public comments) \u2014 pins that auto never silently drops more than rest would and never errors mid-detection"
      ],
      "targets": [
        "client-auth-version-whoami-unparseable-200",
        "client-auth-version-validlogin-network-error",
        "client-auth-version-apply-auth-fallback",
        "client-misc-whoami-fallback-uncovered"
      ],
      "api_modes": [
        "auto",
        "rest",
        "hybrid"
      ]
    },
    {
      "title": "bug update across modes: empty no-op PUT and HTTP-200 error-envelope swallowing detected via read-back",
      "rationale": "bug update always uses REST (mutations are REST-only) but is invoked under all api modes; finding client-bug-update-200-error-swallowed says a 200 error envelope is reported as success. Single cases never (a) run `bug update <id>` with NO change flags then read back to confirm nothing changed, nor (b) attempt an update that the server should reject and verify via read-back that state did NOT change despite a success message. This sequence carries pre/post state across update+view to expose a silent no-op and a swallowed-error update, in both rest and hybrid invocation, which a one-shot command cannot.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Update matrix bug' --description 'd' --priority Normal --op-sys All --rep-platform All : assert exit 0, capture UBUG=.id",
        "bzr bug view $UBUG --fields id,priority,whiteboard : capture PRE_PRIO=.priority, PRE_WB=.whiteboard",
        "bzr bug update $UBUG : capture exit and stderr (no change flags) \u2014 assert EITHER exit==7/non-zero (a guard rejects empty update) OR if exit==0 then the next read-back proves it was a true no-op",
        "bzr bug view $UBUG --fields id,priority,whiteboard : assert .priority == PRE_PRIO AND .whiteboard == PRE_WB (empty update changed nothing)",
        "bzr bug update $UBUG --status RESOLVED --resolution FIXED --comment 'resolving' : assert exit 0 (atomic status+comment)",
        "bzr --api hybrid bug view $UBUG --fields id,status,resolution : assert .status=='RESOLVED' AND .resolution=='FIXED' \u2014 if the server had returned a 200 error envelope, this read-back catches the divergence between the 'Updated' success message and unchanged server state"
      ],
      "targets": [
        "cli-parsing-bug-update-no-fields-empty-put",
        "client-bug-update-200-error-swallowed"
      ],
      "api_modes": [
        "rest",
        "hybrid"
      ]
    },
    {
      "title": "Clone is non-atomic across the create+comment two-step; verify the cloned bug exists even when the follow-on comment path differs by mode",
      "rationale": "clone() creates the bug (REST) then POSTs a 'Cloned from' comment as a separate call; finding commands-bug-clone-comment-failure-after-create says a comment failure makes clone report Err while the bug exists. Existing clone cases (54-57) never verify, after a clone, that exactly one new bug was created and carries comment #0 with the original description, nor do they exercise the no-comment path's effect on description carry-over. This sequence clones, then reads back the clone's comments via hybrid (to see private/redacted comment #0 handling) and asserts the description survived \u2014 catching the silent description-loss and the create/comment split that single commands miss.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Clone matrix source' --description 'ORIGINAL-DESC-MARKER' --priority Highest --severity critical --op-sys Linux --rep-platform PC : assert exit 0, capture CSRC=.id",
        "bzr bug clone $CSRC --op-sys Linux --rep-platform PC : assert exit 0, capture CDST=.id; assert CDST != CSRC",
        "bzr bug view $CDST --fields id,summary,priority : assert .summary=='Clone matrix source', .priority=='Highest'",
        "bzr --api hybrid comment list $CDST : assert exit 0; assert comment with count==0 exists AND its text contains 'ORIGINAL-DESC-MARKER' (description carried into the clone's comment #0, not lost)",
        "bzr comment list $CDST : assert at least one comment text contains 'Cloned from' referencing $CSRC (the second-step comment landed; if clone reported success the link comment must exist)"
      ],
      "targets": [
        "commands-bug-clone-comment-failure-after-create",
        "commands-bug-clone-missing-comment-zero"
      ],
      "api_modes": [
        "rest",
        "hybrid"
      ]
    },
    {
      "title": "create_user mode routing: rest vs xmlrpc vs hybrid produce the same retrievable user (idempotency + fallback divergence)",
      "rationale": "create_user branches per api_mode (rest POST; xmlrpc direct; hybrid REST-then-XMLRPC-on-ANY-error). No functional case creates a user via --api xmlrpc or exercises hybrid's create. This sequence creates a user under explicit rest, then attempts the SAME user under xmlrpc and hybrid (which must idempotently no-op or report 'already exists' consistently), then searches to confirm exactly one user exists. It pins that the three create routes converge on one server-side user rather than the hybrid 'fall back on any error' path silently double-creating or masking a 403 \u2014 a convergence assertion impossible from a single create call.",
      "steps": [
        "bzr --api rest user create --email matrixuser@test.bzr --full-name 'Matrix User' --password 'TestPass1!' : assert EITHER exit 0 OR (non-zero AND stderr contains 'already')",
        "bzr --api xmlrpc user create --email matrixuser@test.bzr --full-name 'Matrix User' --password 'TestPass1!' : assert non-zero AND stderr contains 'already' (xmlrpc route hits the same server-side dedupe)",
        "bzr --api hybrid user create --email matrixuser@test.bzr --full-name 'Matrix User' --password 'TestPass1!' : assert non-zero AND stderr contains 'already' (hybrid REST fails on dup, falls back to XML-RPC which also reports dup \u2014 must NOT report a spurious success)",
        "bzr user search matrixuser : assert exit 0; assert exactly one result whose email == 'matrixuser@test.bzr' (no double-creation across the three routes)"
      ],
      "targets": [
        "client-misc-create-user-hybrid-xmlrpc-uncovered"
      ],
      "api_modes": [
        "rest",
        "xmlrpc",
        "hybrid"
      ]
    },
    {
      "title": "Saved-query round-trip through search-kind: structured filters silently ignored, run-time behavior diverges",
      "rationale": "Finding cli-parsing-query-save-search-filter-no-conflict: `query save --search foo --product X` is accepted, kind becomes Search, structured filters stored but ignored at run time. Existing query cases save list-kind and search-kind separately but never save the CONFLICTING combination then RUN it and compare against a pure-search query to prove the structured filters had no effect. This sequence saves the mixed query, shows it (filters present in the definition), runs it, and runs an equivalent pure-search query \u2014 asserting identical result sets, which proves the stored --product was silently dropped. State carried from save\u2192show\u2192run\u2192compare exposes the precedence bug.",
      "steps": [
        "bzr query save mixed-q --search 'Matrix' --product NonexistentProductXYZ --status NEW --limit 50 : assert EITHER exit non-zero (clap/handler rejects the combination as docs claim) OR exit 0 with .action=='saved' (bug present \u2014 continue to prove it)",
        "bzr query show mixed-q : assert .kind=='search'; assert .search contains 'Matrix'; note whether .product is populated (if populated, the ignored-filter footgun is confirmed in storage)",
        "bzr query save pure-q --search 'Matrix' --limit 50 : assert exit 0, .action=='saved'",
        "bzr query run mixed-q : assert exit 0; capture MIXED_IDS = sorted list of .[].id",
        "bzr query run pure-q : assert exit 0; capture PURE_IDS = sorted list of .[].id; assert MIXED_IDS == PURE_IDS \u2014 identical results prove --product NonexistentProductXYZ was silently ignored (if it had been honored, mixed-q would return zero rows)",
        "bzr query delete mixed-q : assert exit 0; bzr query delete pure-q : assert exit 0"
      ],
      "targets": [
        "cli-parsing-query-save-search-filter-no-conflict",
        "client-bug-search-no-limit-no-param"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "limit boundary semantics carried through saved query: --limit 0 and limit-override across run",
      "rationale": "Finding client-bug-search-no-limit-no-param: limit=Some(0) is untested and Bugzilla treats limit=0 as 'no limit' (opposite of empty). Existing case 81 overrides limit to 1; none test limit 0 or the no-limit path through a saved query. This sequence saves a query, runs it with the saved limit, then runs with --limit 0 override, asserting that 0 returns MORE-or-equal rows than a small positive limit (proving 0 means unbounded, not empty). State: the saved query plus successive overrides reveal the surprising 0-semantics that a single list command never probes.",
      "steps": [
        "bzr query save limit-q --product FuncTestProd --status NEW --status CONFIRMED --status RESOLVED --limit 1 : assert exit 0, .action=='saved'",
        "bzr query run limit-q : assert exit 0; assert (. | length) == 1 (saved limit honored)",
        "bzr query run limit-q --limit 2 : assert exit 0; assert (. | length) <= 2 AND >= 1; capture TWO_N",
        "bzr query run limit-q --limit 0 : assert exit 0; capture ZERO_N; assert ZERO_N >= TWO_N \u2014 if limit=0 returned 0 rows (treated as empty) this fails, exposing the 0-as-unbounded semantic; if it returned all rows it confirms the documented-but-untested behavior",
        "bzr query delete limit-q : assert exit 0"
      ],
      "targets": [
        "client-bug-search-no-limit-no-param",
        "validation-parsing-url-limit-silently-dropped"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Deadline validation parity vs other date flags, with read-back across modes",
      "rationale": "Finding validation-parsing-deadline-unvalidated: --deadline ships raw to the server with no client-side validation, unlike every other date flag (which fails fast exit 7). Case 45b proves --changed-since 'tomorrow' exits 7; no case proves the deadline path's inconsistency. This sequence sets a malformed deadline (should be exit 7 to match siblings, but currently ships to server), then a valid deadline, then reads back via two modes to confirm the valid one landed and the malformed one was either rejected client-side OR produced an opaque server error \u2014 pinning the inconsistency. Carrying the bug's deadline state across set+view is required to distinguish client rejection from silent server acceptance.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Deadline matrix' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture DBUG=.id",
        "bzr bug update $DBUG --deadline 'not-a-date' : capture exit \u2014 assert EITHER exit==7 (client-side validation, matching --changed-since) OR exit==4/non-zero (opaque server rejection); a 0 here with the read-back below proves garbage was accepted",
        "bzr bug view $DBUG --fields id,deadline : assert .deadline is null OR not 'not-a-date' (garbage must NOT have been persisted verbatim)",
        "bzr bug update $DBUG --deadline 2027-03-15 : assert exit 0",
        "bzr --api rest bug view $DBUG --fields id,deadline : assert .deadline == '2027-03-15'",
        "bzr --api hybrid bug view $DBUG --fields id,deadline : assert .deadline == '2027-03-15' (valid deadline reads identically across rest and hybrid)"
      ],
      "targets": [
        "validation-parsing-deadline-unvalidated"
      ],
      "api_modes": [
        "rest",
        "hybrid"
      ]
    },
    {
      "title": "attachment metadata parity (is_patch/is_private/is_obsolete) across rest/xmlrpc/hybrid after a state-changing update",
      "rationale": "Case 99 updates an attachment (obsolete true) and case 100g asserts is_patch in default mode, but no sequence updates an attachment then lists it across ALL THREE modes asserting the boolean fields are reported identically. REST and XML-RPC deserialize these booleans via different parsers (XML-RPC parsing.rs handles <boolean> and self-closing elements differently). This sequence flips is_patch/is_obsolete/is_private via update, then reads the same attachment through rest, xmlrpc, and hybrid, asserting the three booleans match across transports \u2014 catching XML-RPC Event::Empty / boolean-parse divergence that a single-mode list cannot.",
      "steps": [
        "bzr bug create --product FuncTestProd --component Backend --summary 'Attach meta matrix' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture MABUG=.id",
        "write /tmp/bzr-meta.txt 'meta content'",
        "bzr attachment upload $MABUG /tmp/bzr-meta.txt --summary 'meta attach' : assert exit 0, capture MAID=.id",
        "bzr attachment update $MAID --is-patch true --obsolete true : assert exit 0",
        "bzr --api rest attachment list $MABUG : assert exit 0; capture from the entry with id==MAID: R_PATCH=.is_patch, R_OBS=.is_obsolete, R_PRIV=.is_private",
        "bzr --api xmlrpc attachment list $MABUG : assert exit 0; for entry id==MAID assert .is_patch==R_PATCH AND .is_obsolete==R_OBS AND .is_private==R_PRIV (XML-RPC boolean parity)",
        "bzr --api hybrid attachment list $MABUG : assert exit 0; for entry id==MAID assert .is_patch==R_PATCH AND .is_obsolete==R_OBS AND .is_private==R_PRIV"
      ],
      "targets": [
        "xmlrpc-empty-element-events-ignored",
        "client-comment-attachment-update-noop-params"
      ],
      "api_modes": [
        "rest",
        "xmlrpc",
        "hybrid"
      ]
    },
    {
      "title": "group view membership parity across rest/xmlrpc/hybrid after a membership mutation",
      "rationale": "Cases 26/26a view a group via hybrid and rest; finding xmlrpc-extract-attachments-wrong-shape-error notes get_group's xmlrpc shape errors are untested, and create_user/get_group fallback semantics differ. No sequence MUTATES membership (add-user) then reads the group through all three modes asserting the membership array agrees. This sequence adds a user, then views the group via rest, xmlrpc, and hybrid, asserting the new member appears in every transport's membership list \u2014 pinning the get_group XML-RPC struct-parsing path against the REST path for the same just-changed state, which single-mode views can't compare.",
      "steps": [
        "bzr group create --name matrix-grp --description 'matrix group' : assert EITHER exit 0 OR stderr contains 'already exists'",
        "bzr user create --email grpmatrix@test.bzr --full-name 'Grp Matrix' --password 'TestPass1!' : assert EITHER exit 0 OR stderr contains 'already'",
        "bzr user update grpmatrix@test.bzr --disable-login false --login-denied-text '' : assert exit 0",
        "bzr group add-user --group matrix-grp --user grpmatrix@test.bzr : assert exit 0",
        "bzr --api rest group view matrix-grp : assert exit 0; assert .name=='matrix-grp'; assert membership array contains 'grpmatrix@test.bzr'",
        "bzr --api hybrid group view matrix-grp : assert exit 0; assert membership contains 'grpmatrix@test.bzr' (hybrid get_group parity with rest for the same membership state)",
        "bzr group remove-user --group matrix-grp --user grpmatrix@test.bzr : assert exit 0; bzr --api rest group view matrix-grp : assert membership does NOT contain 'grpmatrix@test.bzr'"
      ],
      "targets": [
        "xmlrpc-extract-attachments-wrong-shape-error",
        "client-misc-create-user-hybrid-xmlrpc-uncovered"
      ],
      "api_modes": [
        "rest",
        "hybrid",
        "xmlrpc"
      ]
    },
    {
      "title": "config persistence of xmlrpc api_mode and query_param auth survives reload and drives a real call",
      "rationale": "Finding config-apimode-xmlrpc-serde-roundtrip: api_mode=XmlRpc / auth_method=QueryParam are never saved-then-reloaded to confirm the 'xmlrpc' rename round-trips. Existing config cases set servers but never persist an xmlrpc-defaulted server, reload via a fresh process, and then use it for a network call. This sequence writes a server with explicit query_param auth, confirms config show reflects it, then runs an xmlrpc-routed comment list through that persisted server \u2014 proving the on-disk 'xmlrpc'/'query_param' strings parse back and actually function. The state carried is the config file itself across separate bzr invocations.",
      "steps": [
        "bzr config set-server xrserver --url $BZ_URL --api-key $API_KEY --auth-method query_param --email $ADMIN_EMAIL : assert exit 0",
        "bzr config show : assert exit 0; assert output contains 'xrserver' and reflects query_param auth for that server",
        "bzr bug create --product FuncTestProd --component Backend --summary 'Config roundtrip bug' --description 'd' --op-sys All --rep-platform All : assert exit 0, capture XRBUG=.id (uses default server)",
        "bzr comment add $XRBUG --body 'roundtrip secret' --private : assert exit 0",
        "bzr --server xrserver --api xmlrpc comment list $XRBUG : assert exit 0; assert at least one comment is_private==true (the persisted query_param server authenticates correctly via the reloaded config and the xmlrpc route returns the private comment)",
        "bzr --server xrserver whoami : assert exit 0, assert .id exists (reloaded query_param auth_method actually authenticates)"
      ],
      "targets": [
        "config-apimode-xmlrpc-serde-roundtrip",
        "error-exitcodes-xmlrpc-mapping-untested"
      ],
      "api_modes": [
        "xmlrpc",
        "auto"
      ]
    },
    {
      "title": "Re-run create with same name twice, then verify single resource (product + group idempotency carries state)",
      "rationale": "Existing cases (#9, #25) assert ONE create then accept 'already exists' on a single re-run, but never assert that the second create did NOT mutate or duplicate the resource. This sequence carries the first-run id across a second create and asserts the resource identity is unchanged - a single command can't detect a silent duplicate-create or a second-run that overwrites description/is_active. Targets the create idempotency contract claimed in the index but never verified for invariance.",
      "steps": [
        "bzr --json product create --name IdemProd --description 'first desc' -> assert_success, capture .id as PROD_ID",
        "bzr --json product create --name IdemProd --description 'SECOND desc' -> assert exit is non-zero with 'already exists' on stderr OR exit 0 (record which); MUST NOT create a new product",
        "bzr --json product view IdemProd -> assert_success, assert .id == PROD_ID (server identity unchanged by the second create), assert .description == 'first desc' (second create did NOT overwrite)",
        "bzr --json group create --name idem-grp --description 'g1' --is-active true -> assert_success",
        "bzr --json group create --name idem-grp --description 'g2-CHANGED' --is-active false -> assert duplicate handling (non-zero 'already exists' or 0)",
        "bzr --json group view idem-grp -> assert_success, assert .is_active reflects the FIRST create's value (not flipped by the rejected second create)"
      ],
      "targets": [
        "product create idempotency-on-duplicate invariance (index claims tested, never asserts non-mutation)",
        "group create idempotency invariance",
        "second-create must not silently overwrite description/is_active"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Remove-then-re-add group member, asserting membership state at each transition",
      "rationale": "Cases #28-#32 do add -> list -> remove -> list, but the remove step (#31) only asserts exit 0 and the post-remove list is muddied by an intervening user-disable (#354) so it can't prove the remove itself worked. No case re-adds after a remove. This sequence proves the full add/remove/re-add cycle is idempotent and order-independent WITHOUT disabling the user, so the membership array transitions are the only thing under test. Catches a remove that no-ops, a re-add that fails because internal state thinks the user is still present, or add/remove that aren't true inverses.",
      "steps": [
        "bzr --json user create --email cycle@test.bzr --full-name 'Cycle User' --password Abcd1234! -> assert success or 'already exists'",
        "bzr --json group create --name cycle-grp --description 'membership cycle' -> assert success or 'already exists'",
        "bzr --json group add-user --group cycle-grp --user cycle@test.bzr -> assert_success",
        "bzr --json group list-users --group cycle-grp --details -> assert_success, assert .[] | select(.email==\"cycle@test.bzr\") present (member count includes user)",
        "bzr --json group add-user --group cycle-grp --user cycle@test.bzr -> assert_success (re-add of existing member is idempotent, not an error)",
        "bzr --json group list-users --group cycle-grp --details -> assert_success, assert cycle@test.bzr appears EXACTLY once (no duplicate membership row)",
        "bzr --json group remove-user --group cycle-grp --user cycle@test.bzr -> assert_success",
        "bzr --json group list-users --group cycle-grp --details -> assert_success, assert no row with email==cycle@test.bzr (remove actually took effect)",
        "bzr --json group remove-user --group cycle-grp --user cycle@test.bzr -> assert_success OR documented non-zero (remove of non-member: assert it does not crash and exit code is stable)",
        "bzr --json group add-user --group cycle-grp --user cycle@test.bzr -> assert_success, then group list-users --details asserts the user is back (re-add after remove restores state)"
      ],
      "targets": [
        "group add-user/remove-user true-inverse and idempotency",
        "re-add of existing member produces no duplicate",
        "remove of non-member exit-code stability",
        "membership state survives full add/remove/re-add cycle"
      ],
      "api_modes": [
        "rest",
        "hybrid"
      ]
    },
    {
      "title": "Delete a saved query then run/show it; recreate same name and confirm clean state",
      "rationale": "Cases #83-#85 delete a query and assert show/run fail, but never assert the SPECIFIC exit code (config error vs not-found) and never re-save the same name to prove deletion left no stale residue. This sequence deletes, asserts the precise failure exit code on both run and show, then re-saves the same name with a DIFFERENT definition and confirms the new definition (not the deleted one) is what runs - catching a delete that only tombstones, or a re-save that merges with deleted state. Single commands can't see the delete->recreate state carry.",
      "steps": [
        "bzr --json query save lifecycle-q --product FuncTestProd --status NEW --limit 7 -> assert_success, assert .action==\"saved\"",
        "bzr --json query show lifecycle-q -> assert_success, assert .limit==7, assert .product[0]==\"FuncTestProd\"",
        "bzr --json query delete lifecycle-q -> assert_success, assert .action==\"deleted\"",
        "bzr --json query show lifecycle-q -> assert_failure, assert_exit_code matches the not-found/config exit (record exact code; index never pins it)",
        "bzr --json query run lifecycle-q -> assert_failure, assert exit code identical to the show failure (consistent error class for a missing query)",
        "bzr --json query save lifecycle-q --search 'Clone source bug' --limit 3 -> assert_success, assert .action==\"saved\" (NOT \"updated\" \u2014 proving the delete truly removed it, so this is a fresh create not an update of a tombstone)",
        "bzr --json query show lifecycle-q -> assert_success, assert .kind==\"search\", assert .limit==3, assert .product is null/absent (no residue of the deleted list-kind definition's product filter)"
      ],
      "targets": [
        "query delete leaves no residue (re-save reports 'saved' not 'updated')",
        "query show/run on deleted query exit-code consistency",
        "delete->recreate state isolation between list-kind and search-kind"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Multi-id batch bug update with one invalid id triggers BatchPartialFailure (exit 11) and applies to the valid ids only",
      "rationale": "Case #58 batch-updates two VALID bugs and asserts success; there is NO functional case that mixes a valid and an invalid id to drive the BatchPartialFailure (exit 11) producer in commands/bug/update.rs:243. The audit flags exit 11 / error_type 'batch_partial_failure' and the all-failed table path as untested. This sequence asserts (a) the partial-failure exit code, (b) the JSON envelope's succeeded/failed arrays, (c) that the valid bug WAS actually mutated despite the batch reporting partial failure, and (d) an all-invalid batch. Single-command cases can't observe that a partial failure still committed the good ids.",
      "steps": [
        "bzr --json bug create --product FuncTestProd --component Backend --summary 'batch-valid' --description d --op-sys Linux --rep-platform PC -> capture .id as BVALID",
        "bzr --json bug update $BVALID 999999 --whiteboard partial-batch -> assert_exit_code 11 (BatchPartialFailure), assert .succeeded contains BVALID, assert .failed[] contains id 999999, assert error_type/.type==\"batch_partial_failure\" in JSON error envelope",
        "bzr --json bug view $BVALID -> assert_success, assert .whiteboard==\"partial-batch\" (the valid leg of the batch DID commit even though the batch exited 11)",
        "bzr --json bug update 999998 999999 --whiteboard all-fail-batch -> assert_failure, assert exit code (all-failed: succeeded empty); record whether it is 11 or a different code and assert the JSON envelope has empty succeeded and 2 failed entries",
        "bzr --json bug update $BVALID 999999 --whiteboard with-comment-batch --comment 'batch comment' -> assert_exit_code 11, then bug view $BVALID asserts whiteboard updated AND the comment landed (atomic comment on the surviving id), exposing the '(with comment)' suffix interaction flagged in the audit"
      ],
      "targets": [
        "BatchPartialFailure exit 11 end-to-end (error-exitcodes-batchpartial-errortype-untested)",
        "partial batch still commits valid ids",
        "all-failed batch exit-code and envelope (commands-bug-update-batch-empty-comment-suffix-on-failure)",
        "atomic comment on surviving id in a partial batch"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "config set-default to a server, then delete-effect by removing it from config file, then operate \u2014 default resolution failure ordering",
      "rationale": "Case #6 only tests set-default to a nonexistent server (exit 3). No case sets a server as default, then makes that default unresolvable, then runs a network command to see what default resolution does. bzr has no 'config delete-server' in the index, so deletion is simulated by re-pointing default and overwriting config. This sequence proves the ORDER dependency: a stale default left after the target server vanishes must fail predictably, not silently fall back to another server. Catches default-pointer dangling after the underlying server is gone \u2014 invisible to single commands.",
      "steps": [
        "bzr --json config set-server temp-default --url $BZ_URL --api-key $API_KEY --auth-method query_param --email $ADMIN_EMAIL -> assert_success",
        "bzr --json config set-default temp-default -> assert_success",
        "bzr --json config show -> assert_success, assert the default server field == \"temp-default\"",
        "bzr --json whoami -> assert_success, assert .id exists (default resolves and works)",
        "Remove the [servers.temp-default] block from $XDG_CONFIG_HOME/bzr/config.toml in place (simulating deletion of the server that is still the default), leaving 'default = \"temp-default\"'",
        "bzr --json config show -> assert behavior is defined: either exit 3 (config error: default points to missing server) OR success listing remaining servers; record exact exit code",
        "bzr --json whoami -> assert_failure with config exit code 3 (dangling default must NOT silently fall through to another configured server)",
        "bzr --json config set-default test -> assert_success (recover by repointing default), then whoami -> assert_success (state recovered)"
      ],
      "targets": [
        "set-default + underlying-server-removal dangling-default resolution",
        "default pointer must not silently fall back to a different server",
        "recovery by repointing default",
        "config exit code 3 on unresolvable default at network-command time"
      ],
      "api_modes": [
        "rest",
        "auto"
      ]
    },
    {
      "title": "TOFU pin set on a server, then issuer change drives IssuerChanged (exit 13); pin clear recovers",
      "rationale": "The index covers tls-pin-sha256 set and tls-pin-clear as single config operations, but NEVER the stateful sequence: pin a cert, then present a cert from a DIFFERENT issuer and assert the IssuerChanged/PinMismatch exit (13) on the NEXT network call, then clear the pin and confirm recovery. This is the core TOFU security state machine and the only way to exercise config-set-server-tls-pin-clear (both success and not-found) plus the PinMismatch/IssuerChanged exit-13 path that error_tests asserts only in isolation. Requires a TLS-fronted Bugzilla (or two certs); skip-gate if only http is available.",
      "steps": [
        "Gate: require an https Bugzilla endpoint with a swappable cert; else test_skip 'TOFU requires TLS endpoint'",
        "bzr --json config set-server tofu --url $HTTPS_URL --api-key $API_KEY --tls-pin-sha256 <fingerprint-A> --email $ADMIN_EMAIL -> assert_success",
        "bzr --json --server tofu server info -> assert_success (cert matches pin A, version fetched)",
        "Swap the server's TLS cert to one issued by a different issuer (fingerprint B / issuer changed)",
        "bzr --json --server tofu server info -> assert_exit_code 13 (PinMismatch or IssuerChanged), assert stderr names the issuer change / pin mismatch (drives tls/pin_failure parse_pin_mismatch_details and check_issuer_change)",
        "bzr --json config set-server tofu --tls-pin-clear -> assert_success, assert stderr 'Certificate pin cleared' (config-set-server-tls-pin-clear success branch, currently untested)",
        "bzr --json --server tofu server info -> assert_success (after clearing the pin, TOFU is bypassed and the new cert is accepted, OR re-pins on first use depending on design \u2014 assert the documented behavior, not a crash)",
        "bzr --json config set-server doesnotexist --tls-pin-clear -> assert_failure with config error 'not found \u2014 nothing to clear' (the not-found branch of tls-pin-clear, untested)"
      ],
      "targets": [
        "PinMismatch/IssuerChanged exit 13 end-to-end (error-exitcodes-stale-range-comment, tls-keyring-issuer-with-comma-pin-mismatch)",
        "config-set-server-tls-pin-clear success branch",
        "tls-pin-clear not-found branch",
        "TOFU pin set -> issuer change -> clear recovery ordering"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "bug update with no change flags (empty PUT no-op) and --deadline garbage validation ordering",
      "rationale": "Audit flags two likely bugs: (a) bzr bug update <id> with zero mutating flags builds an empty UpdateBugParams and PUTs it, reporting success for a no-op; (b) --deadline accepts any string with no client-side validation (should be exit 7 like every other date flag). No functional case runs an update with no flags, and no case sends a malformed deadline. This sequence runs them in order against a real bug whose pre-state is captured, so we can assert the no-op did NOT change last_change_time spuriously and that garbage deadline either fails fast (exit 7, desired) or is shipped to the server (current bug \u2014 assert the actual behavior to lock it in).",
      "steps": [
        "bzr --json bug create --product FuncTestProd --component Backend --summary 'noop-target' --description d --op-sys Linux --rep-platform PC -> capture .id as BNOOP",
        "bzr --json bug view $BNOOP --fields id,whiteboard,deadline -> capture baseline (whiteboard empty, deadline null)",
        "bzr --json bug update $BNOOP -> record exit code and message: this is the empty-PUT no-op (cli-parsing-bug-update-no-fields-empty-put). Assert either exit 7 'no fields to update' (desired guard) OR exit 0 'Updated bug' (current bug)",
        "bzr --json bug view $BNOOP --fields id,whiteboard,deadline -> assert whiteboard and deadline unchanged from baseline (no-op truly changed nothing on the server)",
        "bzr --json bug update $BNOOP --deadline garbage-not-a-date -> assert_exit_code 7 (client-side date validation, matching --changed-since etc.); if it returns non-7, that confirms validation-parsing-deadline-unvalidated",
        "bzr --json bug view $BNOOP --fields deadline -> assert .deadline is still null/baseline (garbage deadline was NOT persisted), proving the malformed value never reached a successful write",
        "bzr --json bug update $BNOOP --deadline 2026-12-31 -> assert_success, then bug view asserts .deadline reflects the valid date (the valid path still works after the rejected garbage path)"
      ],
      "targets": [
        "cli-parsing-bug-update-no-fields-empty-put (empty PUT no-op)",
        "validation-parsing-deadline-unvalidated (malformed deadline should be exit 7)",
        "no-op update must not mutate server state",
        "valid deadline still applies after a rejected one"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Clone after deleting source comment #0 / clone with comment-add failure non-atomicity",
      "rationale": "Audit flags two clone bugs: (a) clone reports Err when the 'Cloned from' comment POST fails even though the bug was already created (non-atomic, misleads user into re-cloning -> duplicate); (b) when the source has no comment #0, clone silently produces a description-less clone. No functional case exercises either. This sequence creates a source, clones it normally (capturing the new id), then clones with --no-comment vs without, and re-queries to prove the clone landed and whether description carried. It carries the source->clone id relationship across steps, which a single clone command can't verify.",
      "steps": [
        "bzr --json bug create --product FuncTestProd --component Backend --summary 'clone-src' --description 'original body text' --priority High --severity major --op-sys Linux --rep-platform PC -> capture .id as CSRC",
        "bzr --json bug clone $CSRC --op-sys Linux --rep-platform PC -> assert_success, capture new .id as CLONE1",
        "bzr --json bug view $CLONE1 -> assert_success, assert .summary inherited from CSRC, assert .priority==\"High\"",
        "bzr --json comment list $CLONE1 -> assert_success, assert a comment containing 'Cloned from' and the source id CSRC exists (the two-step create+comment both committed)",
        "bzr --json bug clone $CSRC --no-comment --summary 'clone-no-comment' --op-sys Linux --rep-platform PC -> assert_success, capture .id as CLONE2",
        "bzr --json comment list $CLONE2 -> assert_success, assert NO 'Cloned from' comment (the --no-comment path skips the second POST, so no non-atomic failure window) \u2014 and assert the clone STILL has comment #0 carrying the inherited description (commands-bug-clone-missing-comment-zero)",
        "bzr --json bug view $CLONE2 --fields id,summary -> assert .summary==\"clone-no-comment\" (override applied), proving CLONE2 is a distinct committed bug, not a phantom from a failed two-step"
      ],
      "targets": [
        "commands-bug-clone-comment-failure-after-create (two-step non-atomicity surface)",
        "commands-bug-clone-missing-comment-zero (description carry to clone)",
        "--no-comment skips the second POST cleanly",
        "clone id/relationship roundtrip verification"
      ],
      "api_modes": [
        "rest"
      ]
    },
    {
      "title": "Alias set then collision: reusing an alias across two bugs and asserting ordering of failure",
      "rationale": "The index lists bug update --alias and bug create as covered, but never the stateful collision: assign an alias to bug A, then attempt to assign the SAME alias to bug B (Bugzilla aliases are globally unique). This is a server-side ordering/idempotency constraint that only manifests across two sequential mutations. Asserts the second assignment fails cleanly (not a silent overwrite or a half-applied update mixed with other fields), and that re-assigning the same alias to the SAME bug is idempotent. Single-command coverage can't see the uniqueness collision.",
      "steps": [
        "bzr --json bug create --product FuncTestProd --component Backend --summary 'alias-a' --description d --op-sys Linux --rep-platform PC -> capture .id as BA",
        "bzr --json bug create --product FuncTestProd --component Backend --summary 'alias-b' --description d --op-sys Linux --rep-platform PC -> capture .id as BB",
        "bzr --json bug update $BA --alias uniq-alias-001 -> assert_success",
        "bzr --json bug view $BA --fields id,alias -> assert .alias contains 'uniq-alias-001'",
        "bzr --json bug update $BA --alias uniq-alias-001 --whiteboard wb1 -> assert_success (re-assigning the same alias to the same bug is idempotent and the co-field whiteboard still applies)",
        "bzr --json bug view $BA --fields whiteboard -> assert .whiteboard==\"wb1\" (the non-alias field in the same update committed)",
        "bzr --json bug update $BB --alias uniq-alias-001 --whiteboard wb-should-not-apply -> assert_failure (alias collision rejected by server)",
        "bzr --json bug view $BB --fields alias,whiteboard -> assert .alias does NOT contain 'uniq-alias-001' AND .whiteboard != 'wb-should-not-apply' (the rejected update was atomic \u2014 the co-field whiteboard was NOT partially applied)"
      ],
      "targets": [
        "alias global-uniqueness collision across two bugs",
        "atomicity of a rejected alias update (co-fields not partially applied)",
        "same-alias-same-bug idempotency",
        "bug update --status/--resolution atomicity-with-comment analog for alias+whiteboard"
      ],
      "api_modes": [
        "rest"
      ]
    }
  ]
}