From d25e9d3c07bf59fe64b3817ecab3415790b9ca54 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 09:23:48 +0900 Subject: [PATCH] Fix top-level log flag schema coverage (#3226) --- changelog.d/unreleased/3226.fixed.md | 19 ++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 58 ++++++++++++++++- src/CodeIndex/Cli/ConsoleUi.cs | 70 +++++++++++++++++++-- src/CodeIndex/Cli/ProgramRunner.cs | 26 ++------ tests/CodeIndex.Tests/CliFlagSchemaTests.cs | 37 ++++++++++- tests/CodeIndex.Tests/ConsoleUiTests.cs | 27 +++++++- 6 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 changelog.d/unreleased/3226.fixed.md diff --git a/changelog.d/unreleased/3226.fixed.md b/changelog.d/unreleased/3226.fixed.md new file mode 100644 index 0000000000..fda9313962 --- /dev/null +++ b/changelog.d/unreleased/3226.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3226 +affected: + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/ProgramRunner.cs + - tests/CodeIndex.Tests/CliFlagSchemaTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **Top-level global log flags are now covered by the CLI schema and completion contracts (#3226)** - `--log-format`, `--log-retain-count`, and `--log-max-size-mb` are covered by the shared top-level flag schema, documented in help, and surfaced in shell completions, with tests guarding parser drift. + +## 日本語 + +- **トップレベルの global log flags を CLI schema と completion contract で検証するようになりました (#3226)** - `--log-format`、`--log-retain-count`、`--log-max-size-mb` は共有 top-level flag schema で管理し、help に記載して shell completion に出すようになり、parser との drift をテストで検出します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index b25addc18b..895640b10e 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -30,6 +30,13 @@ internal sealed record CliFlag /// public required IReadOnlySet Commands { get; init; } + /// + /// Whether this flag is accepted before a subcommand and should be surfaced in + /// top-level completion/help contracts. + /// サブコマンド前に受理され、トップレベル補完 / help 契約にも出すフラグかどうか。 + /// + public bool TopLevel { get; init; } + /// /// Commands for which the parser accepts the flag (typically to emit a friendlier /// error like "use --exact-substring on search instead of --exact-name") but for @@ -214,8 +221,17 @@ private static IReadOnlyList BuildAll() new() { Name = "--data-dir", ValuePlaceholder = "", Description = "Directory containing codeindex.db; overrides CDIDX_DATA_DIR/XDG/workspace defaults", Commands = Set(DataDirCommands) }, new() { Name = "--json", Description = "JSON output; search/files/validate also accept --json=array for a single JSON array", Commands = Set(JsonCommands) }, new() { Name = "--format", ValuePlaceholder = "", Description = "Standard output format for token budgets, editor integrations, and CI", Commands = Set(FormatCommands) }, - new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()) }, - new() { Name = "--silent", Description = "Alias for --quiet", Commands = Set(AllCommands.ToArray()) }, + new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()), TopLevel = true }, + new() { Name = "--silent", Description = "Alias for --quiet", Commands = Set(AllCommands.ToArray()), TopLevel = true }, + new() { Name = "--color", ValuePlaceholder = "", Description = "Color output mode", Commands = Set(), TopLevel = true }, + new() { Name = "--palette", ValuePlaceholder = "", Description = "ANSI color palette", Commands = Set(), TopLevel = true }, + new() { Name = "--ascii", Description = "Use ASCII progress glyphs", Commands = Set(), TopLevel = true }, + new() { Name = "--metrics", ValuePlaceholder = "", Description = "Append command metrics JSONL to a file", Commands = Set(), TopLevel = true }, + new() { Name = "--debug-unsafe", Description = "Allow raw debug dumps when CDIDX_DEBUG=unsafe is also set", Commands = Set(), TopLevel = true }, + new() { Name = "--strict-version", Description = "Fail when the workspace version pin does not match this binary", Commands = Set(), TopLevel = true }, + new() { Name = "--log-format", ValuePlaceholder = "", Description = "Persistent stderr log format", Commands = Set(), TopLevel = true }, + new() { Name = "--log-retain-count", ValuePlaceholder = "", Description = "Persistent stderr log file retention count", Commands = Set(), TopLevel = true }, + new() { Name = "--log-max-size-mb", ValuePlaceholder = "", Description = "Persistent stderr log rotation size cap in MiB", Commands = Set(), TopLevel = true }, new() { Name = "--profile", Description = "Emit SQL timing and EXPLAIN QUERY PLAN profile JSON after the normal result", Commands = Set(ProfileCommands) }, new() { Name = "--verbose", Description = "Emit query debug diagnostics to stderr, or _debug JSON when combined with --json", Commands = Set(VerboseQueryCommands.Concat(new[] { "index" }).ToArray()) }, new() { Name = "--notify", ValuePlaceholder = "", Description = "Signal long index completion; desktop currently emits OSC 9 terminal notification", Commands = Set("index") }, @@ -260,7 +276,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--reject-before", ValuePlaceholder = "", Description = "Search: reject primary matches with a nearby guard query before them", Commands = Set("search") }, new() { Name = "--reject-after", ValuePlaceholder = "", Description = "Search: reject primary matches with a nearby guard query after them", Commands = Set("search") }, new() { Name = "--guard-window", ValuePlaceholder = "", Description = "Search: line window for require/reject guard queries", Commands = Set("search") }, - new() { Name = "--no-progress", Description = "Disable animated progress and spinner output", Commands = Set(AllCommands.ToArray()) }, + new() { Name = "--no-progress", Description = "Disable animated progress and spinner output", Commands = Set(AllCommands.ToArray()), TopLevel = true }, new() { Name = "--name", ValuePlaceholder = "", Description = "Exact symbol name", Commands = Set("symbols") }, new() { Name = "--max-line-width", ValuePlaceholder = "", Description = "Clamp long single-line payloads (0 disables clamping)", Commands = Set(MaxLineWidthCommands) }, new() { Name = "--snippet-lines", ValuePlaceholder = "", Description = "Snippet length", Commands = Set("search", "find", "references", "callers", "callees", "impact") }, @@ -346,6 +362,42 @@ public static IReadOnlyList GetCompletionFlagsForCommand(string command return All.Where(f => f.AppliesTo(command)).ToList(); } + /// + /// Flags accepted before a subcommand and surfaced in top-level shell completion. + /// サブコマンド前に受理され、トップレベル補完に出すフラグ集合。 + /// + public static IReadOnlyList GetTopLevelCompletionFlags() + { + return All.Where(f => f.TopLevel).ToList(); + } + + public static HashSet GetTopLevelGlobalOptionNames(bool includeLogOptions) + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var flag in All) + { + if (!flag.TopLevel) + continue; + if (!includeLogOptions && flag.Name.StartsWith("--log-", StringComparison.Ordinal)) + continue; + names.Add(flag.Name); + if (flag.ShortName is not null) + names.Add(flag.ShortName); + } + return names; + } + + public static HashSet GetTopLevelValueOptionNames() + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var flag in All) + { + if (flag is { TopLevel: true, IsValueBearing: true }) + names.Add(flag.Name); + } + return names; + } + /// /// Same allowlist as but partitioned /// into value-bearing options vs flag-only options, for parsers that need to know diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 193ba4221d..baaa74e63f 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -930,6 +930,11 @@ private static void PrintFlagReference(Action WriteHelpLine) WriteHelpLine(" --ascii Use ASCII spinner/progress glyphs instead of Unicode glyphs (also honors CDIDX_ASCII=1, NO_UNICODE, TERM=dumb, accessibility env hints, and non-UTF-8 locales)"); WriteHelpLine(" --no-progress Disable animated progress/spinner output (also honors CDIDX_DISABLE_PROGRESS=1 and PREFERS_REDUCED_MOTION)"); Console.WriteLine(" --metrics Append one JSONL record per CLI command / MCP tool call to (also honors CDIDX_METRICS=)"); + Console.WriteLine(" --log-format Persistent stderr log format (also honors CDIDX_LOG_FORMAT)"); + Console.WriteLine(" --log-retain-count Persistent stderr log file retention count (also honors CDIDX_LOG_RETAIN)"); + Console.WriteLine(" --log-max-size-mb Persistent stderr log rotation size cap in MiB (also honors CDIDX_LOG_MAX_SIZE_MB)"); + WriteHelpLine(" --debug-unsafe Allow raw debug dumps only when CDIDX_DEBUG=unsafe is also set; local troubleshooting only"); + WriteHelpLine(" --strict-version Treat workspace version pin mismatches as exit code 64 instead of warnings"); Console.WriteLine(" --help, -h Show this help message"); Console.WriteLine(" --version, -V Show version information"); Console.WriteLine(" --license Show licensing, trademark, and commercial-use summary"); @@ -1379,6 +1384,7 @@ private static string GetCompletionKinds() => private static string GetBashCompletions() { var cmds = string.Join(" ", Commands); + var topLevelFlags = string.Join(" ", BuildTopLevelFlagList()); var langs = GetCompletionLangs(); var kinds = GetCompletionKinds(); var version = LoadVersion(); @@ -1394,12 +1400,15 @@ private static string GetBashCompletions() sb.Append($" commands=\"{cmds}\"\n"); sb.Append("\n"); sb.Append(" if [ $COMP_CWORD -eq 1 ]; then\n"); - sb.Append(" COMPREPLY=($(compgen -W \"$commands --help --version --license\" -- \"$cur\"))\n"); + sb.Append($" COMPREPLY=($(compgen -W \"$commands --help --version --license {topLevelFlags}\" -- \"$cur\"))\n"); sb.Append(" return\n"); sb.Append(" fi\n"); sb.Append("\n"); sb.Append(" case \"$prev\" in\n"); - sb.Append(" --db|--path|--exclude-path|--output|-o) COMPREPLY=($(compgen -f -- \"$cur\")) ;;\n"); + sb.Append(" --db|--path|--exclude-path|--output|-o|--metrics) COMPREPLY=($(compgen -f -- \"$cur\")) ;;\n"); + sb.Append(" --color) COMPREPLY=($(compgen -W \"auto always never\" -- \"$cur\")) ;;\n"); + sb.Append(" --palette) COMPREPLY=($(compgen -W \"basic 256 truecolor\" -- \"$cur\")) ;;\n"); + sb.Append(" --log-format) COMPREPLY=($(compgen -W \"text json\" -- \"$cur\")) ;;\n"); sb.Append($" --lang) COMPREPLY=($(compgen -W \"{langs}\" -- \"$cur\")) ;;\n"); sb.Append($" --kind) COMPREPLY=($(compgen -W \"{kinds}\" -- \"$cur\")) ;;\n"); sb.Append(" *)\n"); @@ -1485,6 +1494,8 @@ private static string GetZshCompletions() sb.Append(" )\n"); sb.Append("\n"); sb.Append(" _arguments -C \\\n"); + foreach (var arg in BuildZshTopLevelArgs(langs, kinds)) + sb.Append($" {arg} \\\n"); sb.Append(" '1:command:->cmds' \\\n"); sb.Append(" '*::arg:->args'\n"); sb.Append("\n"); @@ -1531,6 +1542,17 @@ private static List BuildZshArgsForCommand(string command, string langs, return args; } + private static List BuildZshTopLevelArgs(string langs, string kinds) + { + var args = new List(); + foreach (var flag in CliFlagSchema.GetTopLevelCompletionFlags()) + args.AddRange(FormatZshArguments(flag, langs, kinds)); + args.Add("'--help[Show help]'"); + args.Add("'--version[Show version]'"); + args.Add("'--license[Show license summary]'"); + return args; + } + private static List BuildZshGenericArgs(string langs, string kinds) { var seen = new HashSet(StringComparer.Ordinal); @@ -1572,6 +1594,9 @@ private static string FormatZshArgument(string name, CliFlag flag, string langs, "" => "datetime", "" => $"language:({langs})", "" => $"kind:({kinds})", + "" => "mode:(auto always never)", + "" => "palette:(basic 256 truecolor)", + "" => "format:(text json)", "" => "query", "" => "name", "" => "address", @@ -1606,6 +1631,21 @@ private static string GetFishCompletions() lines.Add("complete -c cdidx -n '__fish_use_subcommand' -l help -d 'Show help'"); lines.Add("complete -c cdidx -n '__fish_use_subcommand' -l version -d 'Show version'"); lines.Add("complete -c cdidx -n '__fish_use_subcommand' -l license -d 'Show license summary'"); + foreach (var flag in CliFlagSchema.GetTopLevelCompletionFlags()) + { + var name = flag.Name.TrimStart('-'); + var shortName = flag.ShortName is null ? "" : $" -s {flag.ShortName.TrimStart('-')}"; + var requiresArg = flag.IsValueBearing ? " -r" : ""; + var argSpec = flag.ValuePlaceholder switch + { + "" => " -a 'auto always never'", + "" => " -a 'basic 256 truecolor'", + "" => " -a 'text json'", + _ => "", + }; + var description = flag.Description.Replace("'", "\\'"); + lines.Add($"complete -c cdidx -n '__fish_use_subcommand' -l {name}{shortName}{requiresArg}{argSpec} -d '{description}'"); + } // Emit one `complete` line per schema flag, joining the applicable command list into the // fish `__fish_seen_subcommand_from` predicate. Hotspots' `--group-by-name` description is @@ -1615,6 +1655,8 @@ private static string GetFishCompletions() // という対応で生成する。`--group-by-name` のみ既存テストが期待する短い tooltip を維持。 foreach (var flag in CliFlagSchema.All) { + if (flag.Commands.Count == 0) + continue; var commands = string.Join(' ', flag.Commands.OrderBy(c => Array.IndexOf(Commands, c))); var name = flag.Name.TrimStart('-'); // Token order is `-l name (-r)? (-a 'values')? -d 'description'` — matches the @@ -1647,6 +1689,7 @@ private static string GetPowerShellCompletions() var cmds = FormatPowerShellArray(Commands); var langs = FormatPowerShellArray(GetCompletionLangs().Split(' ', StringSplitOptions.RemoveEmptyEntries)); var kinds = FormatPowerShellArray(GetCompletionKinds().Split(' ', StringSplitOptions.RemoveEmptyEntries)); + var topLevelFlags = FormatPowerShellArray(BuildTopLevelFlagList()); var sb = new StringBuilder(); sb.AppendLine($"# cdidx PowerShell completions generated for version {LoadVersion()}"); sb.AppendLine("# Regenerate this script after upgrading cdidx."); @@ -1655,6 +1698,10 @@ private static string GetPowerShellCompletions() sb.AppendLine($" $commands = @({cmds})"); sb.AppendLine($" $langs = @({langs})"); sb.AppendLine($" $kinds = @({kinds})"); + sb.AppendLine(" $colorModes = @('auto', 'always', 'never')"); + sb.AppendLine(" $palettes = @('basic', '256', 'truecolor')"); + sb.AppendLine(" $logFormats = @('text', 'json')"); + sb.AppendLine($" $topLevelFlags = @({topLevelFlags})"); sb.AppendLine(" $elements = @($commandAst.CommandElements)"); sb.AppendLine(" $tokens = @($elements | ForEach-Object { $_.Extent.Text })"); sb.AppendLine(" $lastElement = if ($elements.Count -ge 1) { $elements[$elements.Count - 1] } else { $null }"); @@ -1665,15 +1712,18 @@ private static string GetPowerShellCompletions() sb.AppendLine(" [System.Management.Automation.CompletionResult]::new($value, $value, $kind, $value)"); sb.AppendLine(" }"); sb.AppendLine(" switch ($prev) {"); - sb.AppendLine(" { $_ -in @('--db', '--path', '--exclude-path', '--output', '-o') } {"); + sb.AppendLine(" { $_ -in @('--db', '--path', '--exclude-path', '--output', '-o', '--metrics') } {"); sb.AppendLine(" Get-ChildItem -Name \"$wordToComplete*\" -ErrorAction SilentlyContinue | ForEach-Object { New-CdidxCompletion $_ 'ProviderItem' }"); sb.AppendLine(" return"); sb.AppendLine(" }"); + sb.AppendLine(" '--color' { $colorModes | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ }; return }"); + sb.AppendLine(" '--palette' { $palettes | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ }; return }"); + sb.AppendLine(" '--log-format' { $logFormats | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ }; return }"); sb.AppendLine(" '--lang' { $langs | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ }; return }"); sb.AppendLine(" '--kind' { $kinds | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ }; return }"); sb.AppendLine(" }"); sb.AppendLine(" if (-not $subcmd -or ($tokens.Count -le 2 -and -not ([string]::IsNullOrEmpty($wordToComplete)) -and -not $afterLastToken)) {"); - sb.AppendLine(" $commands + @('--help', '--version', '--license') | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ 'ParameterName' }"); + sb.AppendLine(" $commands + @('--help', '--version', '--license') + $topLevelFlags | Where-Object { $_.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { New-CdidxCompletion $_ 'ParameterName' }"); sb.AppendLine(" return"); sb.AppendLine(" }"); sb.AppendLine(" switch ($subcmd) {"); @@ -1701,6 +1751,18 @@ private static List BuildPowerShellFlagList(string command) return tokens; } + private static List BuildTopLevelFlagList() + { + var tokens = new List(); + foreach (var flag in CliFlagSchema.GetTopLevelCompletionFlags()) + { + tokens.Add(flag.Name); + if (flag.ShortName is not null) + tokens.Add(flag.ShortName); + } + return tokens; + } + private static List BuildPowerShellGenericFlagList() { var seen = new HashSet(StringComparer.Ordinal); diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 125db8b2e7..bdc914c71e 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -26,28 +26,10 @@ internal static class ProgramRunner internal const long TestExtractorMaxInputBytes = 4 * 1024 * 1024; private static readonly TimeSpan InstallerRunTimeout = TimeSpan.FromMinutes(5); private static readonly TimeSpan InstallerKillWaitTimeout = TimeSpan.FromSeconds(5); - private static readonly HashSet NonLogGlobalOptionNames = new(StringComparer.Ordinal) - { - "--quiet", - "-q", - "--silent", - "--color", - "--palette", - "--ascii", - "--no-progress", - "--metrics", - "--debug-unsafe", - "--strict-version", - }; - private static readonly HashSet TopLevelValueOptionNames = new(StringComparer.Ordinal) - { - "--color", - "--palette", - "--metrics", - "--log-format", - "--log-retain-count", - "--log-max-size-mb", - }; + private static readonly HashSet NonLogGlobalOptionNames = + CliFlagSchema.GetTopLevelGlobalOptionNames(includeLogOptions: false); + private static readonly HashSet TopLevelValueOptionNames = + CliFlagSchema.GetTopLevelValueOptionNames(); internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System; private sealed record CommandRunContext( diff --git a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs index 5a392ecd53..9a6f84de78 100644 --- a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs +++ b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs @@ -47,10 +47,10 @@ public void FlagPrimaryAndAlsoAcceptedSets_DoNotOverlap() } [Fact] - public void EveryFlag_AppliesToAtLeastOneCommand() + public void EveryFlag_HasCommandOrTopLevelScope() { foreach (var flag in CliFlagSchema.All) - Assert.NotEmpty(flag.Commands); + Assert.True(flag.Commands.Count > 0 || flag.TopLevel, $"{flag.Name} must apply to a command or top-level scope."); } [Fact] @@ -113,6 +113,30 @@ public void GetAcceptedFlagNamesForCommand_UnionsCommandsAndAlsoAcceptedBy() Assert.DoesNotContain(CliFlagSchema.GetCompletionFlagsForCommand("definition"), f => f.Name == "--exact-substring"); } + [Fact] + public void TopLevelGlobalSchema_IncludesLogFlagsAndMatchesProgramRunnerParserSets() + { + var topLevel = CliFlagSchema.GetTopLevelGlobalOptionNames(includeLogOptions: true); + Assert.Contains("--log-format", topLevel); + Assert.Contains("--log-retain-count", topLevel); + Assert.Contains("--log-max-size-mb", topLevel); + Assert.Contains("--quiet", topLevel); + Assert.Contains("-q", topLevel); + + var valueNames = CliFlagSchema.GetTopLevelValueOptionNames(); + Assert.Contains("--log-format", valueNames); + Assert.Contains("--log-retain-count", valueNames); + Assert.Contains("--log-max-size-mb", valueNames); + + var nonLog = CliFlagSchema.GetTopLevelGlobalOptionNames(includeLogOptions: false); + Assert.DoesNotContain("--log-format", nonLog); + Assert.DoesNotContain("--log-retain-count", nonLog); + Assert.DoesNotContain("--log-max-size-mb", nonLog); + + Assert.Equal(valueNames, GetProgramRunnerStringSet("TopLevelValueOptionNames")); + Assert.Equal(nonLog, GetProgramRunnerStringSet("NonLogGlobalOptionNames")); + } + [Fact] public void VisibilityFilters_AreScopedToSymbolVisibilityCommands() { @@ -247,6 +271,15 @@ private static IReadOnlyList GetConsoleUiCommands() return value!; } + private static HashSet GetProgramRunnerStringSet(string fieldName) + { + var field = typeof(ProgramRunner).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(field); + var value = (HashSet?)field!.GetValue(null); + Assert.NotNull(value); + return value!; + } + private static SortedSet ExtractBashSubcommandFlags(string script, string subcommand) { // Each per-command branch looks like: diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 40ecf7486b..55197977bd 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -703,7 +703,7 @@ public void PrintCompletions_BashCompletesFlagValues() { var output = ConsoleUi.GetCompletionScript("bash"); - Assert.Contains("--db|--path|--exclude-path|--output|-o) COMPREPLY=($(compgen -f -- \"$cur\"))", output); + Assert.Contains("--db|--path|--exclude-path|--output|-o|--metrics) COMPREPLY=($(compgen -f -- \"$cur\"))", output); Assert.Contains("--lang) COMPREPLY=($(compgen -W \"", output); Assert.Contains("csharp", output); Assert.Contains("python", output); @@ -713,6 +713,28 @@ public void PrintCompletions_BashCompletesFlagValues() Assert.Contains("razor_event_binding", output); } + [Fact] + public void PrintCompletions_TopLevelGlobalLogFlagsAcrossShells() + { + var bash = ConsoleUi.GetCompletionScript("bash"); + var zsh = ConsoleUi.GetCompletionScript("zsh"); + var fish = ConsoleUi.GetCompletionScript("fish"); + var powershell = ConsoleUi.GetCompletionScript("powershell"); + + foreach (var flag in new[] { "--log-format", "--log-retain-count", "--log-max-size-mb" }) + { + Assert.Contains(flag, bash); + Assert.Contains(flag, zsh); + Assert.Contains(flag, powershell); + Assert.Contains($"-l {flag.TrimStart('-')}", fish); + } + + Assert.Contains("--log-format) COMPREPLY=($(compgen -W \"text json\" -- \"$cur\"))", bash); + Assert.Contains("'--log-format[Persistent stderr log format]:format:(text json)'", zsh); + Assert.Contains("-l log-format -r -a 'text json'", fish); + Assert.Contains("'--log-format' { $logFormats", powershell); + } + [Fact] public void PrintCompletions_ZshAndFishCompleteKindValuesFromSharedSet() { @@ -1024,6 +1046,9 @@ public void PrintFlagUsage_ShowsFlagsWithoutCommands() Assert.Contains("Index and update options:", output); Assert.Contains("Query options:", output); + Assert.Contains("--log-format ", output); + Assert.Contains("--log-retain-count ", output); + Assert.Contains("--log-max-size-mb ", output); Assert.Contains("--limit , --top ", output); Assert.DoesNotContain("Commands:", output); Assert.DoesNotContain("Examples:", output);