From 28c89f8316e293b932cfbb3dd7f11253c4157dd7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 29 Jul 2026 18:08:27 +0900 Subject: [PATCH 1/2] Reject conflicting search modes (#4879) --- DEVELOPER_GUIDE.md | 8 +++ TESTING_GUIDE.md | 2 + USER_GUIDE.md | 8 +++ changelog.d/unreleased/4879.fixed.md | 23 ++++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 8 +-- .../Cli/QueryCommandRunner.ArgParsing.cs | 16 ++++- .../Cli/QueryCommandRunner.Search.cs | 22 ++++--- tests/CodeIndex.Tests/ConsoleUiTests.cs | 8 +-- .../QueryCommandRunnerSearchHintTests.cs | 59 ++++++++++++------- .../QueryCommandRunnerSearchTests.cs | 7 ++- 10 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 changelog.d/unreleased/4879.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 8e1ad352ea..179e27a76e 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1480,6 +1480,9 @@ Literal-safe `search` queries are bounded in the reader before FTS5 sanitization: maximum 1000 characters and 128 whitespace terms. Keep this guard in `DbReader` so CLI, MCP, and direct reader callers share the same failure mode; raw `--fts` queries continue to use the raw FTS complexity limits instead. +The CLI normalizes exact-mode aliases and then rejects raw `--fts` combined with +`--exact`, `--exact-substring`, or `--token-boundary` before database dispatch. +This keeps query context and replay output on one matching model. When you run: ```sql @@ -4833,6 +4836,11 @@ WHERE fts_chunks MATCH 'content:authenticate' ### 検索の仕組み +CLI は exact-mode alias を正規化してから、raw `--fts` と `--exact`、 +`--exact-substring`、`--token-boundary` の組み合わせを database dispatch 前に +拒否します。これにより query context と replay output は 1 つの一致モデルだけを +保持します。 + 以下のクエリを実行すると: ```sql SELECT f.path, c.start_line, c.content diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 3343749732..d34f87d859 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -201,6 +201,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Excerpt focus coverage reuses one indexed fixture for line-only leading-window behavior, the focus-length dependency, and focus-column range validation; zero and non-numeric focus-column values share one indexed Markdown fixture. Inspect path-line exact/enclosing-symbol cases reuse one indexed source fixture and iterate read-only line queries within a fact. Definition and symbols exact-mode conflict validation share one empty database and a cross-command flag-pair table. + Search raw-FTS/literal-mode conflict coverage shares one pre-dispatch case table across flag ordering, exact aliases, count/issue-draft output, and human/JSON errors; successful issue-draft replay must preserve only its selected matching mode. Symbols compact flag/alias and summary-only JSON envelopes share one editor-format fixture. Symbols JSON array, LSP, quickfix, and SARIF location formats share one editor-format fixture; definition SARIF severity coverage reuses that fixture and asserts informational `note` output separately from warning-level diagnostic output. Validate JSON, compact, count, and SARIF pagination/severity coverage shares one mixed informational/actionable fixture so authoritative totals, limited rows, SARIF levels, and actionability metadata cannot drift across formats; keep missing-`file_issues` degradation coverage in a separate legacy-schema fixture because availability is a distinct mutable state. @@ -1127,6 +1128,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" excerpt の focus coverage は、line-only 時の先頭側 window、focus-length の依存関係、focus-column の範囲検証を1つの indexed fixture で共有してください。focus-column の zero / non-numeric value も1つの indexed Markdown fixture を再利用してください。 inspect path-line の exact/enclosing-symbol case は1つの indexed source fixture を再利用し、read-only line query を fact 内で反復してください。 definition と symbols の exact-mode conflict validation は1つの空databaseとcross-command flag-pair tableを共有してください。 + search の raw-FTS/literal-mode conflict coverage は、flag 順序、exact alias、count/issue-draft output、human/JSON error を1つの pre-dispatch case table で共有してください。成功する issue-draft replay は選択した一致 mode だけを保持することも検証します。 symbols compact flag/aliasとsummary-only JSON envelopeは1つのeditor-format fixtureを共有してください。 symbols JSON array、LSP、quickfix、SARIF location format は1つの editor-format fixture を共有し、definition SARIF severity のテストも同じ fixture を再利用して、情報レベルの `note` 出力を warning レベルの診断出力とは分けて検証してください。 validate の JSON、compact、count、SARIF における pagination / severity coverage は、informational finding と actionable finding が混在する1つの fixture を共有し、authoritative な総件数、limited row、SARIF level、actionability metadata が format 間で drift しないことを検証してください。`file_issues` 欠落時の degradation coverage は availability が独立した mutable state なので、別の legacy-schema fixture に分けてください。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 3262432136..3205b2ea91 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -562,6 +562,10 @@ the release notes will announce the timeline before the alias stops working. MCP mirrors the same split: use `exactSubstring` or `tokenBoundary` on `search`, `exactName` on name-based tools, and keep `exact` only for backward-compatible clients. +Raw `--fts` mode is mutually exclusive with the literal search modes +`--exact`, `--exact-substring`, and `--token-boundary`. Choose one matching +model per search. Conflicting combinations fail with a typed usage error before +query execution, and generated replay commands preserve only the selected mode. In `search --json` and MCP `search` responses, exact substring highlights add `literal_terms` / `literal_term_occurrences` (camelCase in MCP) so clients can render only the requested literal phrase while keeping the broader diagnostic @@ -3810,6 +3814,10 @@ name に対する NFKC + Unicode CaseFold の等価比較です。 削除する予定はありません。削除する場合は、alias が使えなくなる前に release notes で timeline を告知します。MCP も同じ分割を反映します。`search` では `exactSubstring` または `tokenBoundary`、name-based tools では `exactName` を使い、`exact` は後方互換 client 向けに残します。 +raw `--fts` mode と literal search mode の `--exact`、`--exact-substring`、 +`--token-boundary` は相互排他です。検索ごとに一致モデルを 1 つだけ選んでください。 +競合する組み合わせは query 実行前に型付き usage error となり、生成される replay command +には選択した mode だけが保持されます。 `search --json` と MCP `search` の exact substring highlight には `literal_terms` / `literal_term_occurrences`(MCP では camelCase)も追加されるため、 広めの診断用 `terms` / `term_occurrences` を残したまま、要求した literal phrase だけを diff --git a/changelog.d/unreleased/4879.fixed.md b/changelog.d/unreleased/4879.fixed.md new file mode 100644 index 0000000000..56db2b116a --- /dev/null +++ b/changelog.d/unreleased/4879.fixed.md @@ -0,0 +1,23 @@ +--- +category: fixed +issues: + - 4879 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs + - src/CodeIndex/Cli/QueryCommandRunner.Search.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs + - USER_GUIDE.md + - DEVELOPER_GUIDE.md + - TESTING_GUIDE.md +--- + +## English + +- **Search now rejects incompatible raw FTS and literal matching modes (#4879)** — `--fts` can no longer be combined with `--exact`, `--exact-substring`, or `--token-boundary`. Conflicts return a typed usage error before database dispatch, while query context and replay output retain only one matching model. + +## 日本語 + +- **search が互換性のない raw FTS と literal matching mode を拒否するようになりました (#4879)** — `--fts` と `--exact`、`--exact-substring`、`--token-boundary` は同時指定できません。競合は database dispatch 前に型付き usage error となり、query context と replay output は 1 つの一致モデルだけを保持します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 06083c1b7d..f58555fe97 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -526,11 +526,11 @@ private static IReadOnlyList BuildAll() new() { Name = "--fields", ValuePlaceholder = "", Description = "Project bounded-response row fields; inspect selects top-level evidence groups; nested collections accept collection.field", PrimaryCommands = Set(InspectFieldCommands.Concat(BoundedProjectionCommands).ToArray()) }, new() { Name = "--body-only", Description = "Inspect: body-focused JSON shorthand for --body --fields definitions", PrimaryCommands = Set(InspectFieldCommands) }, new() { Name = "--outline-only", Description = "Inspect: outline-first JSON shorthand for --fields file,definitions,nearby_symbols", PrimaryCommands = Set(InspectFieldCommands) }, - new() { Name = "--exact", Description = "Backward-compatible exact shorthand", PrimaryCommands = Set(ExactCommands) }, + new() { Name = "--exact", Description = "Backward-compatible exact shorthand; search mode is incompatible with --fts", PrimaryCommands = Set(ExactCommands) }, new() { Name = "--regex", Description = "Use regular expression matching", PrimaryCommands = Set("find") }, new() { Name = "--exact-name", Description = "Exact symbol-name equality", PrimaryCommands = Set(ExactNameCommands), AlsoAcceptedBy = Set("search") }, - new() { Name = "--exact-substring", Description = "Search-only exact substring match", PrimaryCommands = Set("search"), AlsoAcceptedBy = Set(ExactSubstringAccepted) }, - new() { Name = "--token-boundary", Description = "Search-only exact substring match with identifier/token boundaries", PrimaryCommands = Set("search") }, + new() { Name = "--exact-substring", Description = "Search-only exact substring match; incompatible with --fts", PrimaryCommands = Set("search"), AlsoAcceptedBy = Set(ExactSubstringAccepted) }, + new() { Name = "--token-boundary", Description = "Search-only exact substring match with identifier/token boundaries; incompatible with --fts", PrimaryCommands = Set("search") }, new() { Name = "--prefix", Description = "Trailing-asterisk prefix shorthand", PrimaryCommands = Set("search") }, new() { Name = "--require-before", ValuePlaceholder = "", Description = "Search: require a nearby guard query before each primary match", PrimaryCommands = Set("search") }, new() { Name = "--require-after", ValuePlaceholder = "", Description = "Search: require a nearby guard query after each primary match", PrimaryCommands = Set("search") }, @@ -565,7 +565,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--max-line-width", ValuePlaceholder = "", Description = "Clamp long single-line payloads (0 disables clamping)", PrimaryCommands = Set(MaxLineWidthCommands) }, new() { Name = "--snippet-lines", ValuePlaceholder = "", Description = "Snippet length; issue-drafts accept 0 for path/line-only evidence", PrimaryCommands = Set("search", "audit", "find", "references", "callers", "callees", "impact") }, new() { Name = "--snippet-focus", ValuePlaceholder = "", Description = "Search snippet long-line focus mode", PrimaryCommands = Set("search") }, - new() { Name = "--fts", Description = "Raw FTS5 syntax", PrimaryCommands = Set("search") }, + new() { Name = "--fts", Description = "Raw FTS5 syntax; incompatible with search exact/literal modes", PrimaryCommands = Set("search") }, new() { Name = "--no-dedup", Description = "Show duplicate chunks", PrimaryCommands = Set("search") }, new() { Name = "--no-visibility-rank", Description = "Keep legacy search ranking without symbol visibility weighting", PrimaryCommands = Set("search") }, new() { Name = "--line", ValuePlaceholder = "", Description = "Inspect/excerpt: include one source line as source_excerpt or excerpt window", PrimaryCommands = Set(InspectSourceExcerptCommands) }, diff --git a/src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs b/src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs index 84010b2501..cb9dc1b0a4 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs @@ -848,22 +848,36 @@ private static (TimeSpan Value, string? Error) ResolveStaleAfter(QueryCommandOpt return (DefaultStaleAfter, null); } - private static bool TryResolveSearchExactMode(QueryCommandOptions options, out bool exact, out string? error) + private static bool TryResolveSearchExactMode( + QueryCommandOptions options, + out bool exact, + out string? error, + out string? hint) { if (!TryRejectMultipleExactFlags(options, out error)) { exact = false; + hint = "Choose one search matching mode: --fts, --exact-substring, or --token-boundary. Use --exact only as the backward-compatible alias for --exact-substring."; return false; } if (options.ExactName) { exact = false; error = "Error: --exact-name applies to name-based commands (symbols/definition/references/callers/callees/inspect), not search. Use --exact-substring for search, or keep --exact for backward compatibility."; + hint = "Use --exact-substring or --token-boundary for literal search matching, or remove the exact-name flag."; + return false; + } + if (options.RawFts && (options.Exact || options.ExactSubstring || options.TokenBoundary)) + { + exact = false; + error = "Error: raw FTS mode (--fts) cannot be combined with literal search modes (--exact, --exact-substring, or --token-boundary)."; + hint = "Remove --fts to use literal/exact-substring matching, or remove the exact-mode flag to keep raw FTS5 syntax."; return false; } exact = options.Exact || options.ExactSubstring; error = null; + hint = null; return true; } diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Search.cs b/src/CodeIndex/Cli/QueryCommandRunner.Search.cs index 32e9ab50c4..48a60f1a05 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Search.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Search.cs @@ -61,11 +61,16 @@ private static int RunSearchCore( usageCommandName, options.LanguageValidationError ? jsonOptions : null)) return CommandExitCodes.UsageError; - if (!TryResolveSearchExactMode(options, out var exact, out var exactError)) - { - CommandErrorWriter.WriteStderr(exactError); - return CommandExitCodes.UsageError; - } + if (!TryResolveSearchExactMode(options, out var exact, out var exactError, out var exactHint)) + return CommandErrorWriter.WriteJsonOrHuman( + options.Json, + jsonOptions, + StripErrorPrefix(exactError!), + CommandExitCodes.UsageError, + exactHint, + GetUsageLineOrThrow("search"), + CommandErrorCodes.UsageError, + command: usageCommandName); if (options.OpenIssuesPath != null && options.OutputFormat != OutputFormatIssueDrafts) { WriteUsageError( @@ -231,13 +236,6 @@ private static int RunSearchCore( return CommandExitCodes.UsageError; } var exactSearch = exact || options.TokenBoundary; - if (options.TokenBoundary && options.RawFts) - { - WriteValidationError( - "--token-boundary cannot be combined with --fts.", - "Drop --fts to use exact token-boundary matching, or drop --token-boundary to keep raw FTS5 syntax."); - return CommandExitCodes.UsageError; - } if (exactSearch && options.Prefix) { WriteValidationError( diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index ce543dd15a..ccfbc26b2b 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -1282,7 +1282,7 @@ public void CompletionRenderer_ShellFormatSnapshotMatchesBaseline(string shell, # Regenerate this script after upgrading cdidx. complete -c cdidx -n '__fish_use_subcommand' -a 'index' -d 'index command' complete -c cdidx -n '__fish_use_subcommand' -l license -d 'Show license summary' - complete -c cdidx -n '__fish_seen_subcommand_from search definition references callers callees symbols find inspect' -l exact -d 'Backward-compatible exact shorthand' + complete -c cdidx -n '__fish_seen_subcommand_from search definition references callers callees symbols find inspect' -l exact -d 'Backward-compatible exact shorthand; search mode is incompatible with --fts' complete -c cdidx -n '__fish_seen_subcommand_from search references callers callees find excerpt inspect impact' -l max-line-width -r -d 'Clamp long single-line payloads (0 disables clamping)' """ }, @@ -1339,11 +1339,11 @@ public void CompletionRenderer_FishIncludesFindOptions() // `__fish_seen_subcommand_from ` strings change to the canonical // command-ordering used by `CliFlagSchema.AllCommands`, and descriptions use the // schema's single source of truth (e.g. `--exact` → "Backward-compatible exact - // shorthand"). These assertions intentionally check the schema-ordered groupings + // shorthand; search mode is incompatible with --fts"). These assertions intentionally check the schema-ordered groupings // (`--query` and `--before`/`--after` predicates) and key flag invariants while // accepting the unified wording. // #1570 によりスキーマ駆動。`__fish_seen_subcommand_from` の並びは `CliFlagSchema.AllCommands` - // 順、`--exact` の説明は統一表記 (`Backward-compatible exact shorthand`)。 + // 順、`--exact` の説明は統一表記 (`Backward-compatible exact shorthand; search mode is incompatible with --fts`)。 var output = ConsoleCompletionRenderer.GetCompletionScript("fish"); Assert.Contains("__fish_seen_subcommand_from search recipes definition goto references callers callees symbols files find inspect impact", output); Assert.Contains("__fish_seen_subcommand_from find excerpt", output); @@ -1353,7 +1353,7 @@ public void CompletionRenderer_FishIncludesFindOptions() Assert.Contains("-l query -r -d 'Literal query'", output); Assert.Contains("-l before -r -d 'Context lines before'", output); Assert.Contains("-l after -r -d 'Context lines after'", output); - Assert.Contains("-l exact -d 'Backward-compatible exact shorthand'", output); + Assert.Contains("-l exact -d 'Backward-compatible exact shorthand; search mode is incompatible with --fts'", output); Assert.Contains("__fish_seen_subcommand_from hotspots", output); Assert.Contains("-l group-by-name -d 'Collapse same-name rows across files'", output); } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs index 41672524d0..f914f1dba4 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs @@ -48,34 +48,49 @@ public void RunSearch_ExactSubstringJsonOutputsLiteralHighlightMetadata() } [Fact] - public void RunSearch_ExactSubstringWithRawFtsReportsEffectiveLiteralHighlightMode_Issue3558() + public void RunSearch_RejectsRawFtsWithLiteralModesBeforeDatabaseDispatch_Issue4879() { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_raw_fts_metadata_3558"); + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_fts_literal_conflicts_4879"); try { var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/sql.cs", - "csharp", - "var CommandText = $\"SELECT 1\";\nvar CommandText = other;"); + var cases = new[] + { + new { Args = new[] { "needle", "--db", dbPath, "--fts", "--exact-substring" }, Json = false }, + new { Args = new[] { "--exact-substring", "--fts", "--query", "needle", "--db", dbPath, "--count" }, Json = false }, + new { Args = new[] { "needle", "--db", dbPath, "--exact", "--format", "count", "--fts" }, Json = true }, + new { Args = new[] { "needle", "--db", dbPath, "--format", "issue-drafts", "--exact-substring", "--fts" }, Json = true }, + new { Args = new[] { "needle", "--db", dbPath, "--fts", "--token-boundary", "--json" }, Json = true }, + }; - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["CommandText = $", "--db", dbPath, "--json", "--fts", "--exact-substring"], - _jsonOptions)); + foreach (var testCase in cases) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + testCase.Args, + _jsonOptions)); - using var document = ParseJsonOutput(stdout); - var root = document.RootElement; - var highlight = root.GetProperty("highlights")[0]; - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - Assert.True(root.GetProperty("exact").GetBoolean()); - Assert.False(root.GetProperty("raw_fts").GetBoolean()); - Assert.True(root.GetProperty("literal_highlights_available").GetBoolean()); - Assert.False(root.TryGetProperty("literal_highlight_warning", out _)); - Assert.Equal("CommandText = $", highlight.GetProperty("literal_terms")[0].GetString()); - Assert.Equal("CommandText = $", highlight.GetProperty("literal_term_occurrences")[0].GetProperty("term").GetString()); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + if (testCase.Json) + { + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var root = document.RootElement; + Assert.Equal("error", root.GetProperty("status").GetString()); + Assert.Equal(CommandErrorCodes.UsageError, root.GetProperty("error_code").GetString()); + Assert.Equal("search", root.GetProperty("command").GetString()); + Assert.Contains("raw FTS mode (--fts) cannot be combined with literal search modes", root.GetProperty("message").GetString()); + Assert.Contains("Remove --fts", root.GetProperty("hint").GetString()); + Assert.Contains("cdidx search", root.GetProperty("usage").GetString()); + } + else + { + Assert.Equal(string.Empty, stdout); + Assert.Contains($"Error [{CommandErrorCodes.UsageError}]", stderr); + Assert.Contains("raw FTS mode (--fts) cannot be combined with literal search modes", stderr); + Assert.Contains("Remove --fts", stderr); + Assert.Contains("Usage: cdidx search", stderr); + } + } } finally { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index ac78dbe271..f7be9f91f7 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -229,7 +229,8 @@ public void RunSearch_TokenBoundaryFiltersLongerIdentifiers_Issue4323() } Assert.Equal(CommandExitCodes.UsageError, ftsExitCode); - Assert.Contains("--token-boundary cannot be combined with --fts", ftsStderr); + Assert.Contains($"Error [{CommandErrorCodes.UsageError}]", ftsStderr); + Assert.Contains("raw FTS mode (--fts) cannot be combined with literal search modes", ftsStderr); Assert.Equal(CommandExitCodes.UsageError, recipeExitCode); Assert.Contains("--token-boundary is only supported for ad hoc search", recipeStderr); } @@ -9171,6 +9172,10 @@ public void RunSearch_AdHocIssueDraftsPreserveSourceTotalsSelectorsAndReplay_Iss Assert.Equal("cdidx", replayWords[0]); Assert.Equal("search", replayWords[1]); Assert.Contains("--query", replayWords); + Assert.Contains("--exact-substring", replayWords); + Assert.DoesNotContain("--fts", replayWords); + Assert.DoesNotContain("--exact", replayWords); + Assert.DoesNotContain("--token-boundary", replayWords); Assert.Contains("--total-limit", replayWords); Assert.Contains("--include-generated", replayWords); Assert.Contains("--first-per-file", replayWords); From 510f03426ae0fc34c75c26743831b53c42dbbc7f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 29 Jul 2026 19:33:30 +0900 Subject: [PATCH 2/2] Preserve audit error context (#4879) --- src/CodeIndex/Cli/QueryCommandRunner.Search.cs | 18 ++++++++++++++---- .../QueryCommandRunnerSearchHintTests.cs | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Search.cs b/src/CodeIndex/Cli/QueryCommandRunner.Search.cs index 80a88ad154..4c4d646413 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Search.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Search.cs @@ -81,15 +81,25 @@ private static int RunSearchCore( options.LanguageValidationError || invocationContext.StructuredMachineUsageErrors ? jsonOptions : null)) return CommandExitCodes.UsageError; if (!TryResolveSearchExactMode(options, out var exact, out var exactError, out var exactHint)) + { + var message = StripErrorPrefix(exactError!); + if (invocationContext.StructuredMachineUsageErrors) + { + WriteUsageError(message, options, exactHint!); + return CommandExitCodes.UsageError; + } + return CommandErrorWriter.WriteJsonOrHuman( options.Json, jsonOptions, - StripErrorPrefix(exactError!), + message, CommandExitCodes.UsageError, exactHint, - invocationContext.UsageLine, - CommandErrorCodes.UsageError, - command: invocationContext.CommandName); + usage: null, + errorCode: CommandErrorCodes.UsageError, + command: invocationContext.CommandName, + omitNullUsage: true); + } if (options.OpenIssuesPath != null && options.OutputFormat != OutputFormatIssueDrafts) { WriteUsageError( diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs index f914f1dba4..efa7e2507d 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs @@ -80,7 +80,7 @@ public void RunSearch_RejectsRawFtsWithLiteralModesBeforeDatabaseDispatch_Issue4 Assert.Equal("search", root.GetProperty("command").GetString()); Assert.Contains("raw FTS mode (--fts) cannot be combined with literal search modes", root.GetProperty("message").GetString()); Assert.Contains("Remove --fts", root.GetProperty("hint").GetString()); - Assert.Contains("cdidx search", root.GetProperty("usage").GetString()); + Assert.False(root.TryGetProperty("usage", out _)); } else { @@ -88,7 +88,7 @@ public void RunSearch_RejectsRawFtsWithLiteralModesBeforeDatabaseDispatch_Issue4 Assert.Contains($"Error [{CommandErrorCodes.UsageError}]", stderr); Assert.Contains("raw FTS mode (--fts) cannot be combined with literal search modes", stderr); Assert.Contains("Remove --fts", stderr); - Assert.Contains("Usage: cdidx search", stderr); + Assert.DoesNotContain("Usage:", stderr); } } }