From 514aa02287a669829170ffd59df7f1534363bba6 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 27 Aug 2026 22:33:22 +0900 Subject: [PATCH] Fix executable usage hints for suggestions (#5196) --- TESTING_GUIDE.md | 2 + changelog.d/unreleased/5196.fixed.md | 19 +++++++ src/CodeIndex/Cli/CommandErrorWriter.cs | 28 +++++++--- src/CodeIndex/Cli/SuggestionsCommandRunner.cs | 4 +- .../CommandErrorWriterTests.cs | 13 +++++ tests/CodeIndex.Tests/ProgramCliTests.cs | 51 +++++++++++++++++++ 6 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/5196.fixed.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 8e0d20f0f..b37030e57 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -245,6 +245,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result YAML `outline` integration coverage must assert nested sequence paths, mapping-parent depth, unchanged source ranges and symbol counts, deterministic sorting, and cursor replay across a page boundary. Search-core orchestration coverage must keep validation ahead of database execution, preserve list-recipes > named-query > recipe > plain route precedence and the recipe-field-before-conflict / plain-conflict-before-field aggregation error order, keep count-mode zero/nonzero payloads aligned, retain strict-not-found exit semantics, and order profiled NDJSON as result records, profile metadata, then the terminal record. CLI parsing, command execution, and installer behavior. Index command coverage is split by run mode or feature area, and query command coverage is split by command family with partial test classes so shared console and fixture helpers stay centralized. Keep repeated query-result fixtures, such as overlapping chunk content used by multiple search deduplication tests, in narrow class-level helpers instead of duplicating local builders. `ProgramCliTests.cs` covers top-level entrypoint behavior that must be exercised through a subprocess, while `InstallScriptTests.cs` runs focused bash snippets against `install.sh` in library mode to lock in release-installer regressions without performing real network installs. Installer bundle-generation tests must also verify that `install.sh` is marked generated while every canonical `install_modules/` source remains unmarked. + Suggestions usage-hint coverage for #5196 must keep unknown-option human diagnostics and missing-id / invalid-combination JSON errors aligned on `cdidx suggestions --help`, preserve the existing error-code, category, usage, and exit-code contracts, and assert that the literal `` placeholder is never emitted. `CommandErrorWriterTests.cs` must keep catalog-backed known-command hints and the global fallback for missing, unknown, nested, or control-bearing command identities. Explicit `--files` preflight coverage for #5091 / #5122 keeps real/dry-run and JSON/text failure behavior in one focused contract matrix: bare `--files`, absolute and relative outside-root paths, platform-supported symlink escapes, `none`-mode directory-segment rejection, `internal`/`all` acceptance, nonexistent, directory, filtered, and unsupported targets, canonical duplicates (including target-specific casing and native Unicode/alias spelling), mixed valid/invalid atomicity, raw-token provenance beside `--project` expansion, already-indexed cleanup and existing or deleted reconciliation-control exceptions, generated-code acceptance, unreadable membership snapshots, and the implicit empty full-scan control without `--files`. Include Windows 8.3 ancestor aliases that must map back to established database spelling. Indexed controls replaced by a directory, FIFO, or policy-disallowed symlink must remain cleanup tombstones without opening or following the replacement. TypeScript path-alias configuration tests must cover `none`, inside/outside-root `internal`, and `all`, prove that only regular resolved targets are secure-opened, and keep real/dry-run plus in-process/worker paths aligned. Assert `UsageError`, bounded and redacted per-input reason diagnostics, and unchanged indexed rows and metadata whenever any explicit token is rejected; snapshot-read failures fail closed with a database error. Authoritative C# scoped-update coverage treats bounded parallel extraction as a correctness and resource contract. Keep the `2 * workers` window bound, fixed-worker reuse, whole-window extraction barrier, target-ordered single-writer persistence, and mixed-language serial boundary in the same suite. Required fallbacks cover parallelism one, active symbol filters, content-load seams, post-extraction hooks, non-authoritative or undersized snapshots, ambiguous nullable language reuse, and speculative probe exceptions returning to the serial per-file boundary. Keep serial target disposition in its exact missing/delete, path-filter, indexability/language-probe, unsupported-purge, hardlink, stat-reuse, load/revalidate, reuse-cleanup, persistence, and catch order. Serial/parallel parity must compare summaries, files/chunks/all semantic symbol and reference columns, normalized reference-line and candidate identities, hotspot aggregates, issues, batch-marker cleanup, readiness/user-version metadata, and last-run readable-byte counters across normal, generated, cap, and oversized inputs. Retain all three file-stat barriers. The ordered worker-failure fixture keeps a failed authoritative target, a normal changed target, an unchanged but explicitly targeted authoritative target, and an oversized target in one four-file window; pin the exact persistence-event and commit-hook order, isolated failure phase, incomplete reason, metadata demotion, and separately observed post-update migration-marker cleanup. Cancellation before persistence, after validated load, and at extraction completion must preserve the expected committed prefix, batch ownership, and derived readiness. Global watchdog and direct-fatal tests must assert active phase, bounded return, and terminal side effects; source-negative ordering tests must cover both cap-discarded confirmed evidence and an earlier lexical contract candidate still blocked in symbol extraction. Abnormal-window fixtures release every injected block and wait for the snapshotted all-workers-stopped seam before restoring static hooks or deleting the project root. `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. @@ -1390,6 +1391,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" YAML `outline` の integration coverage では、入れ子 sequence path、mapping parent の depth、不変な source range と symbol count、決定的な sort、page 境界を越える cursor replay を検証してください。 search core の orchestration coverage では、validation が database execution より先に行われること、list-recipes > named-query > recipe > plain の route precedence と recipe では field error が conflict より先・plain では conflict が field error より先になる aggregation error 順、count mode の zero/nonzero payload、strict-not-found の exit semantics、profile 付き NDJSON が result record、profile metadata、terminal record の順になることを維持してください。 CLI の引数解析、コマンド実行、installer 挙動のテスト。Index command coverage は run mode または機能領域ごとの partial suite に分割し、Query command coverage は command family ごとの partial test class に分割して、共有 console / fixture helper は一箇所に保ちます。`ProgramCliTests.cs` はグローバル引数の解釈や完全な CLI 起動フローのように subprocess 経由で確認すべき Program エントリポイント挙動を扱い、`InstallScriptTests.cs` は `install.sh` を library mode で source した bash snippet を実行して、実ネットワーク install を行わずに release installer の回帰を固定する。installer bundle 生成テストでは、`install.sh` が generated と判定される一方、canonical な `install_modules/` source はすべて unmarked のままであることも検証してください。 + #5196 の suggestions usage-hint coverage では、unknown option の human 診断と missing id / invalid combination の JSON error がともに `cdidx suggestions --help` を案内し、既存の error code、category、usage、exit code 契約を維持し、literal `` placeholder を出力しないことを固定してください。`CommandErrorWriterTests.cs` では catalog に基づく既知 command の hint と、command identity が欠落・未知・nested・control character を含む場合の global fallback を維持してください。 #5091 / #5122 の明示的な `--files` preflight coverage では、通常実行 / dry-run と JSON / text の failure behavior を 1 つの focused contract matrix に保ちます。path token のない `--files`、absolute / relative の project root 外 path、対応 platform での symlink escape、`none` mode での directory segment symlink の拒否、`internal` / `all` mode での受理、存在しない target、directory、filter 対象、未対応 target、canonical duplicate(target ごとの casing と native Unicode/alias spelling を含む)、valid / invalid 混在時の atomicity、`--project` 展開と並ぶ raw token provenance、既に index 済みの cleanup、既存または削除済みの reconciliation control の例外、generated code の受理、membership snapshot の読取不能、`--files` のない暗黙の空 full scan control を検証してください。Windows の 8.3 ancestor alias は DB に確立済みの spelling へ戻ることを検証してください。indexed control が directory、FIFO、または policy で禁止された symlink に置換された場合は、置換 object を open / follow せず cleanup tombstone を維持する必要があります。TypeScript path alias configuration test は `none`、root 内外の `internal`、`all` を網羅し、解決後の regular target だけが secure-open されることを証明し、通常実行 / dry-run と in-process / worker path を一致させてください。明示 token が 1 件でも拒否された場合は、`UsageError`、上限付き・伏字化済みの入力別 reason diagnostic、indexed row と metadata の不変性を assertion し、snapshot 読取失敗は database error で fail closed することを確認します。 authoritative C# scoped update の coverage では、bounded parallel extraction を correctness と resource の契約として扱います。`2 * workers` の window 上限、固定 worker の再利用、window 全体の extraction barrier、target 順の single-writer persistence、mixed-language の serial 境界を同じ suite で維持してください。必須 fallback は parallelism 1、active symbol filter、content-load seam、post-extraction hook、non-authoritative / target 不足 snapshot、ambiguous な nullable language reuse、speculative probe 例外から serial per-file boundary への復帰を含みます。serial target disposition は missing/delete、path filter、indexability / language probe、unsupported purge、hardlink、stat reuse、load / revalidate、reuse cleanup、persistence、catch の厳密な順序を維持してください。serial / parallel parity は normal、generated、cap、oversized input を横断し、summary、file / chunk、symbol / reference の全 semantic column、正規化した reference-line / candidate identity、hotspot aggregate、issue、batch marker cleanup、readiness / user-version metadata、last-run readable-byte counter を比較してください。3段階の file-stat barrier も維持します。順序付き worker-failure fixture では、失敗する authoritative target、通常の変更 target、明示対象だが変更なしの authoritative target、oversized target を4-file windowにまとめ、persistence event と commit hook の厳密な順序、分離された failure phase、incomplete reason、metadata demotion、update後に独立観測する migration marker cleanupを固定してください。persistence 前、validated load 後、extraction completion 時の cancellation は、想定 committed prefix、batch ownership、derived readiness を固定します。global watchdog と direct fatal は active phase、bounded return、terminal side effect を検証し、source-negative ordering は cap で payload から除かれた confirmed evidence と、symbol extraction 中に block した earlier lexical contract candidate の両方を含めます。異常 window の fixture は注入 block をすべて解放し、snapshot 済み all-workers-stopped seam を待ってから static hook の復元と project root の削除を行ってください。 `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。 diff --git a/changelog.d/unreleased/5196.fixed.md b/changelog.d/unreleased/5196.fixed.md new file mode 100644 index 000000000..11154e745 --- /dev/null +++ b/changelog.d/unreleased/5196.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 5196 +affected: + - TESTING_GUIDE.md + - src/CodeIndex/Cli/CommandErrorWriter.cs + - src/CodeIndex/Cli/SuggestionsCommandRunner.cs + - tests/CodeIndex.Tests/CommandErrorWriterTests.cs + - tests/CodeIndex.Tests/ProgramCliTests.cs +--- + +## English + +- **Usage errors now point to executable help commands (#5196)** — command-aware fallback hints now direct suggestions errors to `cdidx suggestions --help`, while unavailable or unrecognized command identities safely fall back to `cdidx --help`; human and JSON output no longer expose the internal `` placeholder. + +## 日本語 + +- **usage error が実行可能な help command を案内するようになりました (#5196)** — command-aware な fallback hint により suggestions のエラーは `cdidx suggestions --help` を案内し、command identity が不明または未認識の場合は安全に `cdidx --help` へフォールバックするため、human / JSON 出力に内部の `` placeholder が露出しなくなりました。 diff --git a/src/CodeIndex/Cli/CommandErrorWriter.cs b/src/CodeIndex/Cli/CommandErrorWriter.cs index 842d59d9e..d8a9d89be 100644 --- a/src/CodeIndex/Cli/CommandErrorWriter.cs +++ b/src/CodeIndex/Cli/CommandErrorWriter.cs @@ -6,7 +6,6 @@ namespace CodeIndex.Cli; internal static class CommandErrorWriter { - internal const string DefaultHint = "Run ' --help' for usage information."; internal const string ResponseBudgetCategory = "response_budget"; internal const string MinimumResponseBytesUnavailableBeforeMaterialization = "normal_payload_not_materialized"; internal const string MinimumResponseBytesUncertainRuntimeEnvelope = "runtime_metadata_or_embedded_budget_varies_between_invocations"; @@ -22,11 +21,25 @@ internal static void WriteStderr(string? message = "") internal static void WriteWarning(string message) => WriteStderr($"Warning: {message}"); - internal static void Write(string message, string? hint = null, string? usage = null, string? errorCode = null) + internal static string BuildUsageHint(string? canonicalCommand = null) + { + var command = canonicalCommand != null + && CliCommandMetadata.PublicCommandNames.Contains(canonicalCommand, StringComparer.Ordinal) + ? $"cdidx {canonicalCommand}" + : "cdidx"; + return $"Run `{command} --help` for usage information."; + } + + internal static void Write( + string message, + string? hint = null, + string? usage = null, + string? errorCode = null, + string? command = null) { var prefix = errorCode is null ? "Error" : $"Error [{errorCode}]"; WriteStderr($"{prefix}: {message}"); - WriteStderr($"Hint: {hint ?? DefaultHint}"); + WriteStderr($"Hint: {hint ?? BuildUsageHint(command)}"); if (usage != null) WriteStderr(FormatUsage(usage)); } @@ -36,9 +49,10 @@ internal static int Write( int exitCode, string? hint = null, string? usage = null, - string? errorCode = null) + string? errorCode = null, + string? command = null) { - Write(message, hint, usage, errorCode); + Write(message, hint, usage, errorCode, command); return exitCode; } @@ -75,7 +89,7 @@ internal static int WriteJsonOrHuman( return exitCode; } - Write(message, exitCode, hint, usage, errorCode); + Write(message, exitCode, hint, usage, errorCode, command); return exitCode; } @@ -97,7 +111,7 @@ internal static JsonObject BuildJsonPayload( new CommandErrorJsonResult( "error", message, - hint ?? DefaultHint, + hint ?? BuildUsageHint(command), resolvedErrorCode, path, resolvedCategory, diff --git a/src/CodeIndex/Cli/SuggestionsCommandRunner.cs b/src/CodeIndex/Cli/SuggestionsCommandRunner.cs index 8cdccb75d..45ea827ef 100644 --- a/src/CodeIndex/Cli/SuggestionsCommandRunner.cs +++ b/src/CodeIndex/Cli/SuggestionsCommandRunner.cs @@ -1289,7 +1289,7 @@ private static int WriteUsageError(string message, bool json, JsonSerializerOpti jsonOptions, message, CommandExitCodes.UsageError, - hint ?? CommandErrorWriter.DefaultHint, + hint ?? CommandErrorWriter.BuildUsageHint("suggestions"), Usage); private static int WritePreflightError( @@ -1304,7 +1304,7 @@ private static int WritePreflightError( result.Error!, result.ExitCode, usageFailure - ? CommandErrorWriter.DefaultHint + ? CommandErrorWriter.BuildUsageHint("suggestions") : "Retry the GitHub lookup after resolving the reported remote failure; duplicate preflight is indeterminate and no issue drafts were exported.", usageFailure ? Usage : null, category: result.ErrorCategory, diff --git a/tests/CodeIndex.Tests/CommandErrorWriterTests.cs b/tests/CodeIndex.Tests/CommandErrorWriterTests.cs index d34b87642..f3ced98cd 100644 --- a/tests/CodeIndex.Tests/CommandErrorWriterTests.cs +++ b/tests/CodeIndex.Tests/CommandErrorWriterTests.cs @@ -124,6 +124,19 @@ public void ResolveMachineContract_NotFoundUsesGenericCodeWithoutDomainContext_I Assert.Equal("not_found", category); } + [Theory] + [InlineData("suggestions", "Run `cdidx suggestions --help` for usage information.")] + [InlineData(null, "Run `cdidx --help` for usage information.")] + [InlineData("not-a-command", "Run `cdidx --help` for usage information.")] + [InlineData("suggestions show", "Run `cdidx --help` for usage information.")] + [InlineData("suggestions\n--version", "Run `cdidx --help` for usage information.")] + public void BuildUsageHint_UsesCanonicalCommandOrSafeGlobalFallback_Issue5196( + string? canonicalCommand, + string expected) + { + Assert.Equal(expected, CommandErrorWriter.BuildUsageHint(canonicalCommand)); + } + [Fact] public void Write_DoesNotDuplicateExistingUsagePrefix_Issue4244() { diff --git a/tests/CodeIndex.Tests/ProgramCliTests.cs b/tests/CodeIndex.Tests/ProgramCliTests.cs index ddc7f5389..b2290419f 100644 --- a/tests/CodeIndex.Tests/ProgramCliTests.cs +++ b/tests/CodeIndex.Tests/ProgramCliTests.cs @@ -1152,6 +1152,57 @@ public void Suggestions_AddHelpDocumentsWriteContract_Issue4422() Assert.Contains("Examples:", stdout); } + [ProductionRuntimeFact] + public void Suggestions_UsageErrorsUseExecutableHelpHints_Issue5196() + { + using var fixture = SuggestionFixture.Create(); + const string expectedHint = "Run `cdidx suggestions --help` for usage information."; + + var (humanExitCode, humanStdout, humanStderr) = RunCliInSubprocess([ + "suggestions", "list", "--bogus", "--db", fixture.DbPath + ]); + var (missingIdExitCode, missingIdStdout, missingIdStderr) = RunCliInSubprocess([ + "suggestions", "show", "--db", fixture.DbPath, "--json" + ]); + var (invalidCombinationExitCode, invalidCombinationStdout, invalidCombinationStderr) = RunCliInSubprocess([ + "suggestions", "list", "--actor", "agent", "--db", fixture.DbPath, "--json" + ]); + + Assert.Equal(CommandExitCodes.UsageError, humanExitCode); + Assert.Equal(string.Empty, humanStdout); + Assert.Contains("Error: --bogus is not supported for suggestions.", humanStderr, StringComparison.Ordinal); + Assert.Contains($"Hint: {expectedHint}", humanStderr, StringComparison.Ordinal); + Assert.Contains("Usage: cdidx suggestions", humanStderr, StringComparison.Ordinal); + Assert.DoesNotContain("", humanStderr, StringComparison.Ordinal); + + Assert.Equal(CommandExitCodes.UsageError, missingIdExitCode); + Assert.Equal(string.Empty, missingIdStderr); + using (var document = JsonDocument.Parse(missingIdStdout)) + { + var root = document.RootElement; + Assert.Equal("suggestions show requires an id.", root.GetProperty("message").GetString()); + Assert.Equal(expectedHint, root.GetProperty("hint").GetString()); + Assert.Equal(CommandErrorCodes.UsageError, root.GetProperty("error_code").GetString()); + Assert.Equal("usage", root.GetProperty("category").GetString()); + Assert.Equal(CommandExitCodes.UsageError, root.GetProperty("exit_code").GetInt32()); + Assert.Contains("Usage: cdidx suggestions", root.GetProperty("usage").GetString(), StringComparison.Ordinal); + } + Assert.DoesNotContain("", missingIdStdout, StringComparison.Ordinal); + + Assert.Equal(CommandExitCodes.UsageError, invalidCombinationExitCode); + Assert.Equal(string.Empty, invalidCombinationStderr); + using (var document = JsonDocument.Parse(invalidCombinationStdout)) + { + var root = document.RootElement; + Assert.Contains("--actor and --reason can only be used", root.GetProperty("message").GetString(), StringComparison.Ordinal); + Assert.Equal(expectedHint, root.GetProperty("hint").GetString()); + Assert.Equal(CommandErrorCodes.UsageError, root.GetProperty("error_code").GetString()); + Assert.Equal("usage", root.GetProperty("category").GetString()); + Assert.Equal(CommandExitCodes.UsageError, root.GetProperty("exit_code").GetInt32()); + } + Assert.DoesNotContain("", invalidCombinationStdout, StringComparison.Ordinal); + } + [ProductionRuntimeFact] public void Suggestions_ListJsonSupportsLimitAndOffset() {