Skip to content

Commit 0f8b8a1

Browse files
committed
Address Copilot review comments on PR #440
- `--messaging-endpoint`: fail fast when supplied without its precondition (`--m365` on `setup all`; `--endpoint-only` on `setup blueprint`) instead of silently ignoring it. - Distinguish "option omitted" from "explicitly passed empty" — an empty `--messaging-endpoint ""` now errors with exit 1 rather than being treated as omitted (which would prompt/defer/no-op). - `setup blueprint --endpoint-only` now captures the registration result and exits non-zero when the endpoint did not register, so scripting is reliable. - Add handler-invocation tests for the three guards. - review-staged skill: add generic Rules W (CLI option precondition enforcement), X (Option<string?> empty-vs-omitted conflation), and Y (ignored result-bearing call / missing exit code) so future reviews catch this class of issue.
1 parent ef4403a commit 0f8b8a1

4 files changed

Lines changed: 112 additions & 2 deletions

File tree

.claude/skills/review-staged/SKILL.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,19 @@ Nine checks that static per-file analysis tends to miss — each requires tracin
134134

135135
Implementation: at the start of the review, emit the list of branch-level files and treat them as the review surface. At the end, verify each file was read or explicitly justified as "no rule applies."
136136

137-
Full detection rules and real examples are in `.claude/agents/pr-code-reviewer.md` Step 9, Rules N through V.
137+
- **CLI option precondition enforcement (Rule W)**: **Added 2026-06-02 after PR #440's Copilot review.** When a new or modified `Option<...>` has a description that constrains its applicability — phrases like `(--X only)`, `use with --Y`, `Only meaningful with --Z`, `requires an existing ...` — the handler **must validate that precondition and fail fast** (`logger.LogError(...) + context.ExitCode = 1 + return`) when the option is supplied outside that context. A flag that is accepted and then silently ignored (because the code path it feeds is skipped) is a bug: the user gets exit 0 and assumes it took effect.
138+
139+
Detection: for every option whose description contains a constraint keyword, grep the handler for a guard that errors when the option is present but the precondition is false. Use `context.ParseResult.CommandResult.FindResultFor(<option>) != null` to detect "supplied" (not the value, which can't tell omitted from empty). If no such guard exists, flag it. **Severity: MEDIUM.** PR #440 example: `--messaging-endpoint` was documented `(--m365 only)` and `with --endpoint-only`, but both handlers accepted it unconditionally and dropped it when the messaging step / endpoint-only path was skipped.
140+
141+
- **`Option<string?>` empty-vs-omitted conflation (Rule X)**: **Added 2026-06-02 after PR #440.** When a string option is read as `GetValueForOption(opt)?.Trim()` and then gated only with `string.IsNullOrWhiteSpace(...)`, an **explicitly-passed empty value** (`--opt ""` or `--opt " "`) is indistinguishable from **omitted**. This is a defect whenever "omitted" triggers a *different* behavior than "empty should" — e.g. omitted falls back to config / prompts / defers, so `--opt ""` silently does the fallback instead of erroring.
142+
143+
Detection: for each `Option<string?>` whose omitted-path does something non-trivial (prompt, default, defer, config fallback), verify the handler distinguishes "specified" via `FindResultFor(opt) != null` and emits a targeted error + `ExitCode = 1` when specified-but-whitespace. This is the CLAUDE.md "Input Validation" rule applied to the explicit-empty case. **Severity: MEDIUM.** PR #440 example: `--messaging-endpoint ""` was `.Trim()`ed to `""`, read as omitted, and triggered prompting/deferral instead of a clear error.
144+
145+
- **Ignored result-bearing call / missing exit code (Rule Y)**: **Added 2026-06-02 after PR #440.** Extends the exit-code-completeness check (CLAUDE.md item 8) to *discarded return values*. When a command handler `await`s an operation that returns a success/failure-bearing result (an enum like `*Result`, a `bool`, a tuple with an outcome) and **does not capture or branch on it**, any failure that result encodes will still exit 0 — breaking scripting/CI.
146+
147+
Detection: in every command handler, find `await SomeOp(...)` calls whose return type is non-`void`/non-`Task` and whose result is not assigned or checked. For each, confirm the failure values map to `context.ExitCode = 1`. **Severity: MEDIUM** (HIGH if the command is commonly scripted). PR #440 example: the `--endpoint-only` path called `RegisterEndpointAndSyncAsync(...)` (returns `EndpointRegistrationResult`) and discarded it, so `NotConfigured` / contract-mismatch / `Failed` all exited 0.
148+
149+
Full detection rules and real examples are in `.claude/agents/pr-code-reviewer.md` Step 9, Rules N through V (plus W–Y above).
138150

139151
### Context Awareness
140152
The skill differentiates between:

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,19 @@ public static Command CreateCommand(
198198
var tenantIdFlag = context.ParseResult.GetValueForOption(tenantIdOption);
199199
bool isM365 = context.ParseResult.GetValueForOption(m365Option);
200200
var authMode = context.ParseResult.GetValueForOption(authModeOption)?.ToLowerInvariant();
201+
// Distinguish "option omitted" from "option explicitly passed empty" — the latter must be a
202+
// hard error, not silently treated as omitted (which would prompt/defer instead).
203+
var messagingEndpointSpecified = context.ParseResult.CommandResult.FindResultFor(messagingEndpointOption) != null;
201204
var messagingEndpointFlag = context.ParseResult.GetValueForOption(messagingEndpointOption)?.Trim();
202205
var ct = context.GetCancellationToken();
203206

207+
if (messagingEndpointSpecified && string.IsNullOrWhiteSpace(messagingEndpointFlag))
208+
{
209+
logger.LogError("--messaging-endpoint requires an HTTPS URL value (e.g. https://my-agent.example.com/api/messages).");
210+
context.ExitCode = 1;
211+
return;
212+
}
213+
204214
// --messaging-endpoint validation: must be a well-formed HTTPS URL when supplied.
205215
if (!string.IsNullOrWhiteSpace(messagingEndpointFlag) &&
206216
(!Uri.TryCreate(messagingEndpointFlag, UriKind.Absolute, out var msgEndpointUri) ||
@@ -377,6 +387,15 @@ effectiveAuthModeForValidation is not ("obo" or "s2s" or "both"))
377387
if (nonDwConfig is null)
378388
isM365 = true;
379389

390+
// --messaging-endpoint only takes effect for M365 agents (the messaging endpoint step is
391+
// skipped otherwise). Fail fast rather than silently ignoring the supplied value.
392+
if (messagingEndpointSpecified && !isM365)
393+
{
394+
logger.LogError("--messaging-endpoint applies only to M365 agents. Add --m365 (or use --aiteammate).");
395+
context.ExitCode = 1;
396+
return;
397+
}
398+
380399
if (nonDwConfig is not null)
381400
{
382401
if (dryRun)

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ public static Command CreateCommand(
204204
var skipEndpointRegistration = context.ParseResult.GetValueForOption(skipEndpointRegistrationOption);
205205
var endpointOnly = context.ParseResult.GetValueForOption(endpointOnlyOption);
206206
var updateEndpoint = context.ParseResult.GetValueForOption(updateEndpointOption);
207+
// Distinguish "option omitted" from "explicitly passed empty" — the latter is a hard error,
208+
// not silently treated as omitted (which would later no-op or fall back to config).
209+
var messagingEndpointSpecified = context.ParseResult.CommandResult.FindResultFor(messagingEndpointOption) != null;
207210
var messagingEndpointFlag = context.ParseResult.GetValueForOption(messagingEndpointOption)?.Trim();
208211
var skipRequirements = context.ParseResult.GetValueForOption(skipRequirementsOption);
209212
var isM365 = context.ParseResult.GetValueForOption(m365Option);
@@ -287,6 +290,22 @@ public static Command CreateCommand(
287290
return;
288291
}
289292

293+
if (messagingEndpointSpecified && string.IsNullOrWhiteSpace(messagingEndpointFlag))
294+
{
295+
logger.LogError("--messaging-endpoint requires an HTTPS URL value (e.g. https://my-agent.example.com/api/messages).");
296+
context.ExitCode = 1;
297+
return;
298+
}
299+
300+
// --messaging-endpoint is consumed only by the --endpoint-only registration path. Fail fast
301+
// rather than accepting a value that would be silently ignored on a normal blueprint run.
302+
if (messagingEndpointSpecified && !endpointOnly)
303+
{
304+
logger.LogError("--messaging-endpoint applies only with --endpoint-only. To replace an existing endpoint, use --update-endpoint <url>.");
305+
context.ExitCode = 1;
306+
return;
307+
}
308+
290309
// --messaging-endpoint validation: must be a well-formed HTTPS URL when supplied.
291310
if (!string.IsNullOrWhiteSpace(messagingEndpointFlag) &&
292311
(!Uri.TryCreate(messagingEndpointFlag, UriKind.Absolute, out var msgEndpointUri) ||
@@ -398,7 +417,7 @@ await RequirementsSubcommand.RunChecksOrExitAsync(
398417
return;
399418
}
400419

401-
await RegisterEndpointAndSyncAsync(
420+
var endpointResult = await RegisterEndpointAndSyncAsync(
402421
config.FullName,
403422
logger,
404423
configService,
@@ -407,6 +426,14 @@ await RegisterEndpointAndSyncAsync(
407426
overrideEndpointUrl: messagingEndpointFlag,
408427
correlationId: correlationId,
409428
cancellationToken: ct);
429+
430+
// Non-zero exit when the endpoint did not actually register (not configured, contract
431+
// mismatch, or other failure) so scripts and CI can detect it.
432+
if (endpointResult != Models.EndpointRegistrationResult.Created &&
433+
endpointResult != Models.EndpointRegistrationResult.AlreadyExists)
434+
{
435+
context.ExitCode = 1;
436+
}
410437
return;
411438
}
412439

src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,58 @@ public void DryRunDisplay_ShouldShowMessagingUrl()
591591

592592
#endregion
593593

594+
#region --messaging-endpoint option validation
595+
596+
[Fact]
597+
public async Task SetupBlueprint_MessagingEndpointWithoutEndpointOnly_ExitsOne()
598+
{
599+
var command = BlueprintSubcommand.CreateCommand(
600+
_mockLogger, _mockConfigService, _mockExecutor, _mockAuthValidator, _mockPlatformDetector,
601+
_mockBackendConfigurator, _mockGraphApiService, _mockBlueprintService, _mockClientAppValidator, _mockBlueprintLookupService, _mockFederatedCredentialService);
602+
var parser = new CommandLineBuilder(command).Build();
603+
604+
var result = await parser.InvokeAsync(
605+
new[] { "--messaging-endpoint", "https://agent.contoso.com/api/messages" }, new TestConsole());
606+
607+
result.Should().Be(1,
608+
because: "--messaging-endpoint only applies with --endpoint-only; supplying it alone must fail fast, not be silently ignored");
609+
}
610+
611+
[Fact]
612+
public async Task SetupBlueprint_EndpointOnlyWithEmptyMessagingEndpoint_ExitsOne()
613+
{
614+
var command = BlueprintSubcommand.CreateCommand(
615+
_mockLogger, _mockConfigService, _mockExecutor, _mockAuthValidator, _mockPlatformDetector,
616+
_mockBackendConfigurator, _mockGraphApiService, _mockBlueprintService, _mockClientAppValidator, _mockBlueprintLookupService, _mockFederatedCredentialService);
617+
var parser = new CommandLineBuilder(command).Build();
618+
619+
var result = await parser.InvokeAsync(
620+
new[] { "--endpoint-only", "--m365", "--messaging-endpoint", "" }, new TestConsole());
621+
622+
result.Should().Be(1,
623+
because: "an explicitly-empty --messaging-endpoint must error, not be treated as omitted");
624+
}
625+
626+
[Fact]
627+
public async Task SetupBlueprint_EndpointOnlyWhenEndpointNotConfigured_ExitsOne()
628+
{
629+
// Blueprint exists but no messaging endpoint is configured → registration returns Failed.
630+
var config = new Agent365Config { TenantId = "test-tenant", AgentBlueprintId = "blueprint-123" };
631+
_mockConfigService.LoadAsync(Arg.Any<string>()).Returns(Task.FromResult(config));
632+
633+
var command = BlueprintSubcommand.CreateCommand(
634+
_mockLogger, _mockConfigService, _mockExecutor, _mockAuthValidator, _mockPlatformDetector,
635+
_mockBackendConfigurator, _mockGraphApiService, _mockBlueprintService, _mockClientAppValidator, _mockBlueprintLookupService, _mockFederatedCredentialService);
636+
var parser = new CommandLineBuilder(command).Build();
637+
638+
var result = await parser.InvokeAsync(new[] { "--endpoint-only", "--m365", "--skip-requirements" }, new TestConsole());
639+
640+
result.Should().Be(1,
641+
because: "endpoint registration that doesn't complete must surface a non-zero exit code for scripting");
642+
}
643+
644+
#endregion
645+
594646
#region RegisterEndpointAndSyncAsync Tests
595647

596648
[Fact]

0 commit comments

Comments
 (0)