Repository navigation
refactor: address review feedback on the Azure CLI subscription lookup - #350
Conversation
Follow-up to #349. No behavioural change on the happy path. - Injectable resolver. ValidateAndResolveSubscription takes an optional Func<string> for the subscription lookup, defaulting to the Azure CLI. The previous test could only assert when 'az' resolution happened to fail, so it was a no-op on any machine with a logged-in Azure CLI, and forcing failure by mutating PATH/ComSpec would have been flaky given test parallelization is enabled. Success, throwing and non-GUID paths are now covered deterministically. - Startup failure message. It reported process.StartInfo.FileName, which on Windows is the command interpreter, so it read as though cmd.exe were the Azure CLI. It now names 'az' and describes the interpreter separately as the launcher. - Command interpreter resolution. ComSpec is trimmed of surrounding quotes, since ProcessStartInfo.FileName is not parsed as a command line and a quoted value would fail to start. The fallback resolves cmd.exe from the system directory instead of relying on a PATH lookup. - Timeout path. After Kill the process is given a short grace period to actually terminate and the in-flight pipe reads are observed, rather than throwing immediately and leaving both dangling. Also extracts ParseSubscriptionId so the exit-code, non-JSON and missing-id branches are unit-testable without spawning a process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Minimum allowed coverage is Generated by 🐒 cobertura-action against ad80d6d |
There was a problem hiding this comment.
Pull request overview
Refactors Azure CLI default subscription lookup to be more testable and diagnosable, while preserving the existing “happy path” behavior for subscription auto-resolution across commands.
Changes:
- Adds an injectable subscription-ID resolver seam to
CommandHelpers.ValidateAndResolveSubscriptionto enable deterministic success/failure tests without relying on a local Azure CLI install. - Improves Windows process-launch robustness and diagnostics in
AzCommand(command interpreter resolution, clearer “could not start” messaging, timeout cleanup, and extracted outcome parsing). - Expands unit test coverage for interpreter resolution, launcher description, and subscription-id parsing edge cases.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/AzureCostCli.Tests/Infrastructure/AzCommandTests.cs | Adds focused unit tests for interpreter resolution, launcher description, and subscription-id parsing branches. |
| tests/AzureCostCli.Tests/Commands/CommandHelpersTests.cs | Replaces the previously environment-dependent failure-path check with deterministic resolver seam tests. |
| src/Infrastructure/AzCommand.cs | Refactors Azure CLI invocation for clearer diagnostics, safer Windows launcher resolution, timeout cleanup, and testable parsing. |
| src/Commands/CommandHelpers.cs | Introduces optional resolveSubscriptionId parameter to make subscription resolution deterministic in tests without changing call sites. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Give the reads a moment to complete against the now-closed pipes and observe | ||
| // any faults, so they are not left as unobserved task exceptions. | ||
| Task.WaitAll(readTasks, KillTimeout); |
Task.WaitAll(tasks, timeout) returns false on timeout rather than throwing, so a read faulting after the grace period elapsed was left unobserved -- exactly what KillAndObserve claimed to prevent. Faulted continuations are now attached up front, so observation no longer depends on the wait completing. Extracts ObserveFaults so the behaviour is covered by tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Good catch — fixed in ad80d6d.
internal static void ObserveFaults(params Task[] tasks)
{
foreach (var task in tasks)
{
_ = task.ContinueWith(
static faulted => _ = faulted.Exception,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}Extracted as One scoping note for the record: the practical impact here is low, since unobserved I have not asserted the GC-finalization property itself (i.e. that 306 tests pass. |
Follow-up to #349, addressing all four review comments. No behavioural change on the happy path — verified the subscription still auto-resolves and returns real cost data.
1. Injectable resolver — the substantive one
ValidateAndResolveSubscriptionnow takes an optionalFunc<string>for the subscription lookup, defaulting toAzCommand.GetDefaultAzureSubscriptionId. All 11 call sites are unchanged.The reviewer was right that the old test was vacuous on any machine with a logged-in Azure CLI — including mine, so CI green never actually proved the reason-appending worked; only a manual repro did. They were also right that forcing failure by mutating
PATH/ComSpecin-process would be flaky, sinceAssemblyInfo.cs:3setsDisableTestParallelization = false. That correctly rules out the cheap fix, so the seam is the answer.Three deterministic tests replace it: resolver succeeds, resolver throws (reason must survive into the message), resolver returns a non-GUID (must be reported as a distinct failure).
2. Startup failure message named the wrong binary
The message reported
process.StartInfo.FileName, which on Windows is the command interpreter — so it read "Unable to start the Azure CLI ('cmd.exe')" and pointed a maintainer at entirely the wrong executable. It now namesazand describes the interpreter separately:Non-Windows is unchanged (no launcher suffix, since
azis invoked directly).DescribeLauncheris internal so this is directly tested — otherwise the message would only be reachable whencmd.exeitself fails to start, i.e. never in practice.3. Command interpreter resolution
ComSpecis trimmed of surrounding quotes:ProcessStartInfo.FileNameis not parsed as a command line, so a quoted value would be taken as part of the file name and fail to start. The fallback now resolvescmd.exefrom the system directory rather than relying on a PATH lookup.Worth noting this is hardening rather than a fix for an observed failure —
COMSPECis system-set to an unquoted absolute path, so a quoted value means something already went out of its way to break it. It's two lines and strictly more predictable, so it's not worth arguing about.4. Timeout path
After
Kill, the process now gets a 2-second grace period to actually terminate, and the two in-flight pipe reads are observed rather than left dangling.Half of this comment was overstated — unobserved
Taskexceptions have not torn down the process since .NET 4.5, so that part was benign. The legitimate half is throwing whileazmay still be shutting down, which is now handled. This path is also the least likely to ever execute.Extra: testable outcome parsing
ParseSubscriptionId(exitCode, stdout, stderr)is extracted so the exit-code, non-JSON and missing-idbranches are unit-testable without spawning a process — these previously had no coverage at all. This also covers the--output jsonregression directly: a table-formatted response now has an explicit test asserting the "not valid JSON" diagnostic.Testing
304 tests pass (292 → 304: one environment-dependent test removed, 13 deterministic ones added). Manually re-verified on macOS that the happy path and the
az-absent path both still behave correctly.As with #349, the Windows runtime path itself remains unexecuted — no Windows machine, CI is ubuntu-only. Everything about it is unit-tested at the argument/message level, but confirmation from a Windows user on #347 is still the thing that would actually close the loop.
🤖 Generated with Claude Code