Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
- `IndexCommandRunner*Tests.cs`, `QueryCommandRunner*Tests.cs`, `ProgramCliTests.cs`, `InstallScriptTests.cs`
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.
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.
`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.
Status-explain coverage must enumerate the source-generated `StatusResult` serializer properties and require every serialized top-level key to succeed without opening a database. Keep major readiness/trust/extension/maintenance/cap-hit metadata assertions, dotted-member resolution, bounded valid unknown candidates, and redaction of hostile field input in the same focused fixture so serialization and explainability cannot drift independently. Keep every structured explain response key registered for status `--fields` projection, and exercise the response through a bounded JSON projection that proves the outer envelope also omits runtime and path metadata.
Safety-recipe semantic coverage must keep safe and unsafe controls in separate indexed files: assert that `Regex.Escape` / `Regex.Unescape` and literal `UseShellExecute=false` are absent, while matching, source-defined, or unresolved Regex members, literal `true`, and propagated shell-policy values remain with classification evidence. Include alias trivia, alias-like comments and strings, a qualified BCL call in a file with a bare `BoundedRegex` alias, a line break before literal `false`, and computed continuations after block and line comments. Also retain a bare receiver from a legacy index without current reference identity, add enough safe helpers to saturate the normal result envelope, keep count output aligned with full JSON, and retain a separate `new Regex` construction positive.
Graph snippet-length validation uses one command/format matrix across `references`, `callers`, and `callees`. Keep explicit `--snippet-lines` fail-fast without `--body`, reject location-only and count output even when `--body` is present, preserve the zero/range errors, and retain successful text/JSON body excerpts through the accepted maximum. Also cover option-like literals supplied through `--query` and `--`, plus two-page bounded body projection with clean stderr, correct total counts, and cursor continuation. Top-level compact aliases must validate the original arguments before database access, and bounded count replay must preserve snippet-like query literals.
Expand Down Expand Up @@ -1154,6 +1155,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- `IndexCommandRunner*Tests.cs`、`QueryCommandRunner*Tests.cs`、`ProgramCliTests.cs`、`InstallScriptTests.cs`
YAML `outline` の integration coverage では、入れ子 sequence path、mapping parent の depth、不変な source range と symbol count、決定的な sort、page 境界を越える cursor replay を検証してください。
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 のままであることも検証してください。
`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 で維持してください。
status explain の coverage は source-generated `StatusResult` serializer property を列挙し、database を開かずに serialized top-level key がすべて成功することを必須にします。主要な readiness / trust / extension / maintenance / cap-hit metadata、dot 区切り member resolution、unknown key に対する上限付きの有効な candidate、悪意ある field input の redaction を同じ focused fixture に置き、serialization と explainability が独立して drift しないようにしてください。structured explain response の全 key を status の `--fields` projection に登録し、outer envelope からも runtime / path metadata が省略されることを上限付き JSON projection で固定してください。
safety recipe の意味論 coverage では、安全側と危険側の control を別々の indexed file に置いてください。`Regex.Escape` / `Regex.Unescape` と literal `UseShellExecute=false` が除外され、matching、source-defined、または解決不能な Regex member、literal `true`、伝播された shell-policy 値が classification evidence 付きで残ることを検証します。alias の trivia、alias に見える comment / string、bare `BoundedRegex` alias と完全修飾 BCL call が同居する file、literal `false` の前の改行、block / line comment の後に続く計算式を含めます。また、現行 reference identity を持たない legacy index の bare receiver を残すこと、通常の result envelope を埋める数の safe helper、count 出力と full JSON の一致、別の `new Regex` construction 正例も維持してください。
graph の snippet 長 validation は、`references`、`callers`、`callees` を横断する1つの command / format matrix で検証します。明示した `--snippet-lines` は `--body` なしで fail-fast し、`--body` があっても location-only / count 出力では拒否すること、zero / range error を維持すること、受理上限まで text / JSON の body excerpt が成功することを固定してください。さらに、`--query` と `--` で渡した option 風 literal、および stderr が空で total count と cursor continuation が正しい2ページの bounded body projection も検証してください。top-level compact alias は database access より前に原引数を検証し、bounded count replay は snippet 風の query literal を維持しなければなりません。
Expand Down
18 changes: 18 additions & 0 deletions changelog.d/unreleased/4976.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 4976
affected:
- TESTING_GUIDE.md
- src/CodeIndex/Cli/JsonEnvelopeWrapper.cs
- src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs
- tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs
---

## English

- **Global JSON-envelope preprocessing now preserves option-shaped command data (#4976)** — a parser-accepted separated option value (notably `--query`) or positional value after `--` that equals `--json-envelope` is no longer consumed as the global envelope flag, while a genuine flag after a missing option value still produces a structured JSON envelope.

## 日本語

- **グローバル JSON envelope の前処理が option 形式の command data を保持するようになりました (#4976)** — parser が受理する分離形式の option 値(特に `--query`)または `--` 後の positional 値が `--json-envelope` と同じ場合でもグローバル envelope flag として消費されず、option 値が欠けた後の本物の flag は構造化 JSON envelope を生成します。
90 changes: 58 additions & 32 deletions src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ private static bool IsBoundedResponseRequest(string command, string[] args)

return HasArgument(args, "--fields")
|| HasArgument(args, "--cursor")
|| (command == "search" && HasEnvelopeFlag(args) && HasJsonArrayOutputSelection(args))
|| (command != "search" && HasEnvelopeFlag(args) && HasArgument(args, "--max-json-bytes"))
|| (command == "search" && HasEnvelopeFlag(command, args) && HasJsonArrayOutputSelection(args))
|| (command != "search" && HasEnvelopeFlag(command, args) && HasArgument(args, "--max-json-bytes"))
|| ShouldAutoWrapBoundedResponse(command, args);
}

Expand All @@ -123,7 +123,7 @@ private static bool IsStandaloneFindCountContinuationRequest(string[] args)
&& !HasArgument(args, "--fields")
&& !HasArgument(args, "--max-json-bytes")
&& !HasCompactOutputSelection(args)
&& !HasEnvelopeFlag(args);
&& !HasEnvelopeFlag("find", args);

private static bool IsFindCountResponseRequest(string[] args)
{
Expand Down Expand Up @@ -265,7 +265,7 @@ private static int RunBoundedResponse(
if (command == "map" && ValidateMapProjectionControls(args, controls.Fields) is { } mapProjectionError)
return WriteBoundedResponseUsageError(mapProjectionError, "Remove the conflicting map filter, or select a collection enabled by --sections.");

var queryNormalized = ExtractQueryArg(args);
var queryNormalized = ExtractQueryArg(command, args);
var (resolvedDbPath, dbPathExplicit) = ResolveQueryDbPath(args);
var queryFingerprint = BuildResponseFingerprint(command, args);
var suppressRuntimeMetadata = IsStaticStatusExplainRequest(command, args);
Expand Down Expand Up @@ -1334,26 +1334,28 @@ private static bool ReadOptionalBool(JsonObject obj, string propertyName, bool d

private static string[] PrepareBoundedInnerArgs(string command, string[] args, BoundedResponseControls controls)
{
var stripped = StripResponseOptions(args, stripLimit: PageableResponseCommands.Contains(command));
var stripped = StripResponseOptions(command, args, stripLimit: PageableResponseCommands.Contains(command));
var bodyRequested = HasExplicitBodyProjection(controls.Fields);
if (command != "outline"
&& !bodyRequested
&& (controls.Compact || controls.Fields is { Count: > 0 }))
{
stripped.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal));
}
var additions = new List<string>();
if (PageableResponseCommands.Contains(command))
{
stripped.Add("--limit");
stripped.Add(controls.PageLimit.ToString(CultureInfo.InvariantCulture));
additions.Add("--limit");
additions.Add(controls.PageLimit.ToString(CultureInfo.InvariantCulture));
}
if (controls.Compact
&& (command == "map" || LegacyLocationCompactCommands.Contains(command) && !bodyRequested))
{
stripped.Add("--format");
stripped.Add("compact");
additions.Add("--format");
additions.Add("compact");
}
stripped.Add("--json");
additions.Add("--json");
InsertBeforeEndOfOptions(command, stripped, additions);
return [.. stripped];
}

Expand Down Expand Up @@ -1428,18 +1430,20 @@ private static bool HasExplicitBodyProjection(IReadOnlyList<string>? fields)

private static string[] PrepareCountArgs(string command, string[] args)
{
var stripped = StripResponseOptions(args, stripLimit: true);
var stripped = StripResponseOptions(command, args, stripLimit: true);
stripped.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal)
|| string.Equals(arg, "--summary-only", StringComparison.Ordinal)
|| string.Equals(arg, "--strict-not-found", StringComparison.Ordinal));
RemoveParsedGraphSnippetLinesOption(stripped);
var additions = new List<string>();
if (command == "impact")
{
stripped.Add("--limit");
stripped.Add(MaxPageWindow.ToString(CultureInfo.InvariantCulture));
additions.Add("--limit");
additions.Add(MaxPageWindow.ToString(CultureInfo.InvariantCulture));
}
stripped.Add("--count");
stripped.Add("--json");
additions.Add("--count");
additions.Add("--json");
InsertBeforeEndOfOptions(command, stripped, additions);
return [.. stripped];
}

Expand Down Expand Up @@ -1474,35 +1478,57 @@ private static void RemoveParsedGraphSnippetLinesOption(List<string> args)
}
}

private static List<string> StripResponseOptions(string[] args, bool stripLimit)
private static List<string> StripResponseOptions(string command, string[] args, bool stripLimit)
{
var stripped = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (string.Equals(arg, EnvelopeFlag, StringComparison.Ordinal)
|| string.Equals(arg, "--compact", StringComparison.Ordinal)
|| string.Equals(arg, "--pretty", StringComparison.Ordinal)
|| string.Equals(arg, "--json", StringComparison.Ordinal)
|| arg.StartsWith("--json=", StringComparison.Ordinal)
|| arg.StartsWith("--fields=", StringComparison.Ordinal)
|| arg.StartsWith("--cursor=", StringComparison.Ordinal)
|| arg.StartsWith("--max-json-bytes=", StringComparison.Ordinal)
|| arg.StartsWith("--format=", StringComparison.Ordinal)
|| (stripLimit && (arg.StartsWith("--limit=", StringComparison.Ordinal) || arg.StartsWith("--top=", StringComparison.Ordinal))))
var tokens = ClassifyArgumentTokens(command, args).ToArray();
for (var i = 0; i < tokens.Length; i++)
{
var token = tokens[i];
var arg = token.Value;
if (token.IsOption
&& (string.Equals(arg, EnvelopeFlag, StringComparison.Ordinal)
|| string.Equals(arg, "--compact", StringComparison.Ordinal)
|| string.Equals(arg, "--pretty", StringComparison.Ordinal)
|| string.Equals(arg, "--json", StringComparison.Ordinal)
|| arg.StartsWith("--json=", StringComparison.Ordinal)
|| arg.StartsWith("--fields=", StringComparison.Ordinal)
|| arg.StartsWith("--cursor=", StringComparison.Ordinal)
|| arg.StartsWith("--max-json-bytes=", StringComparison.Ordinal)
|| arg.StartsWith("--format=", StringComparison.Ordinal)
|| (stripLimit && (arg.StartsWith("--limit=", StringComparison.Ordinal) || arg.StartsWith("--top=", StringComparison.Ordinal)))))
continue;
if (IsResponseValueOption(arg, stripLimit) && i + 1 < args.Length)
if (token.IsOption && IsResponseValueOption(arg, stripLimit) && i + 1 < tokens.Length)
{
i++;
continue;
}
if (string.Equals(arg, "--count", StringComparison.Ordinal))
if (token.IsOption && string.Equals(arg, "--count", StringComparison.Ordinal))
continue;
stripped.Add(arg);
}
return stripped;
}

private static void InsertBeforeEndOfOptions(
string command,
List<string> args,
IReadOnlyCollection<string> additions)
{
var insertionIndex = args.Count;
var index = 0;
foreach (var token in ClassifyArgumentTokens(command, [.. args]))
{
if (token.IsOption && string.Equals(token.Value, "--", StringComparison.Ordinal))
{
insertionIndex = index;
break;
}
index++;
}
args.InsertRange(insertionIndex, additions);
}

private static bool IsResponseValueOption(string arg, bool includeLimit)
=> arg is "--fields" or "--cursor" or "--max-json-bytes" or "--format"
|| includeLimit && arg is "--limit" or "--top";
Expand Down Expand Up @@ -1698,7 +1724,7 @@ private static string BuildResponseFingerprint(string command, string[] args)
var scanMode = command == "find"
? IsFindCountResponseRequest(args) ? "count" : "rows"
: null;
var normalized = StripResponseOptions(args, stripLimit: true);
var normalized = StripResponseOptions(command, args, stripLimit: true);
normalized.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal));
normalized.RemoveAll(arg => arg is "--allow-partial" or "--results-only" or "--verbose" or "--profile");
RemoveOptionWithValue(normalized, "--line-scan-limit");
Expand Down
Loading
Loading