Skip to content

fix(cli): skip update check for unknown subcommands to fail fast#1541

Open
maro114510 wants to merge 3 commits into
microsoft:mainfrom
maro114510:fix/fail-fast-invalid-command
Open

fix(cli): skip update check for unknown subcommands to fail fast#1541
maro114510 wants to merge 3 commits into
microsoft:mainfrom
maro114510:fix/fail-fast-invalid-command

Conversation

@maro114510

@maro114510 maro114510 commented May 29, 2026

Copy link
Copy Markdown

Thank you for maintaining this project. I opened this PR to fix a small issue I noticed.

Description

The update check ran in the CLI group handler, which Click invokes before resolving the subcommand name.
As a result, even a typo like apm updsate would block on a GitHub API call (synchronous, up to 2 s timeout) before the No such command error was shown.
The docstring said "non-blockingly" but the call was fully synchronous.

The guard ctx.command.get_command(ctx, ctx.invoked_subcommand) is not None routes through Click's public command-resolution API (Group.get_command), which is the same mechanism Click uses internally when dispatching subcommands. This makes the guard robust to future plugin or lazy-loaded commands, while keeping the logic consistent with Click's own resolution path.
One side effect: bare apm with no subcommand also skips the check now (invoked_subcommand is None), which is intentional since there's nothing to execute.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

@maro114510

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@maro114510 maro114510 marked this pull request as ready for review May 29, 2026 05:56
Copilot AI review requested due to automatic review settings May 29, 2026 05:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the CLI’s update-notification behavior so it only runs for recognized subcommands, and adds integration tests to verify update checks are skipped for invalid/unknown commands.

Changes:

  • Gate _check_and_notify_updates() behind a “known subcommand” check in the CLI entrypoint.
  • Add integration tests covering invalid command, valid command, and no-subcommand scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
tests/integration/test_version_notification.py Adds integration tests ensuring the update check is skipped for invalid/unknown commands and when no subcommand is provided.
src/apm_cli/cli.py Changes update-check gating logic to run only when a recognized subcommand is invoked.

Comment thread src/apm_cli/cli.py
Comment on lines +67 to 69
# Check for updates only for known commands; skip on invalid input to fail fast.
if not ctx.resilient_parsing and ctx.invoked_subcommand in cli.commands:
_check_and_notify_updates()
danielmeppiel added a commit to danielmeppiel/genesis that referenced this pull request May 29, 2026
…S discipline

Dispatched v0.3+ panel against microsoft/apm#1541 (+41/-2, 2 files, small CLI fix).

Results:
- Executor cost: ~$0.21 (vs $2.85 on PR #1424)
- Per-kLoC: $5.12 (vs $1.15 baseline) — fixed Sonnet-executor overhead
  dominates at small scale (~93% of total cost)
- Panel cost shape holds in dollar terms; per-kLoC ratio inverted
- Arbiter trigger correctly did NOT fire (0 BLOCKERs)

KEY EMPIRICAL VALIDATION of v0.3.4 PER-LENS DIFFERENTIATION:
The executor reflected per-lens against the CAPABILITY PROFILE template
and concluded 4/5 lenses genuinely TRIVIAL, but security lens was
INADEQUATE on TRIVIAL/Haiku — it surfaced a real MEDIUM bypass concern
but could not validate it without out-of-diff function body access.

This empirically generates the per-element justification the v0.3.4
corpus requires architects to record at design time. Recommended carve-out:
'Security lens uses Haiku when all referenced functions are in-diff;
escalates to REVIEWER with tool access when it must reason about
out-of-diff internals.'

Implication for PR #1424: security lens was likely mis-bound to TRIVIAL.
The blocker false-positive (_substitute_plugin_root alleged undefined,
refuted only via out-of-diff gh api lookup) is consistent with TRIVIAL-
class inadequacy on cross-file reasoning. A v0.3.4 re-architect would
correctly bind security to REVIEWER, expected cost delta +$0.50-1.00
per run with measurable security finding fidelity improvement.

REPORT updated with multi-scenario section (small-PR + different-skill).
Deferral list narrowed: full S1-S5 × {v0.2,v0.3+} matrix remains follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 19, 2026
@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

@maro114510 thank you for your contribution. Could you please check Copilot's feedback for this PR? I also requested an APM Panel Review, which might provide extra feedback that needs fixing (focus only on blocking). Finally, make sur ewe get a green on all CI tasks. Once everythins is fixed will approve.

Addresses Copilot's feedback: routes the known-command check through
Click's public resolution API rather than inspecting the internal
commands dict directly, making the guard robust to future dynamic
command loading.
auto-merge was automatically disabled June 19, 2026 18:34

Head branch was pushed to by a user without write access

@maro114510

Copy link
Copy Markdown
Author

@sergio-sisternes-epam

Thanks for the review!

  • Copilot’s feedback: addressed — changed ctx.invoked_subcommand in cli.commands to ctx.command.get_command(ctx, ctx.invoked_subcommand) is not None so it routes through lick’s public resolution API. I also updated the PR description to reflect this.
  • CI: all checks are green.
  • APM Panel Review: the agent job has been IN_PROGRESS for over 24 hours and appears to be hung. Could you re-trigger it when you get a chance?

@github-actions

Copy link
Copy Markdown

APM Review Panel: ship_with_followups

Guard clause is correct and well-motivated; add CHANGELOG entry (P6) and validate the unknown-command test is not vacuous before merging.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

The one-line guard is correct. All active mandatory specialist panelists agree the behavioral outcome is right: mature CLI tools in the ecosystem do not emit update banners on help display or invalid input, and APM should match that convention. The change leaves the update nudge intact on every real workflow command -- init, install, run -- which are the adoption-funnel touchpoints. supply-chain-security-expert confirms the change is security-neutral to slightly positive, narrowing the frequency of outbound version-check calls by restricting them to the recognized-command whitelist. This is an external-contributor PR: it deserves a clean, low-friction path to merge.

Two recommended findings carry enough weight to flag before merge. First, doc-writer correctly identifies a missing CHANGELOG entry. The [Unreleased] block is empty and this PR makes a user-observable behavioral change: the startup update notification is now suppressed on unknown commands and bare invocations. P6 is explicit -- "every transformation has a name and a line in the changelog" -- and this qualifies. Second, test-coverage-expert raises a structurally valid concern about test_no_update_check_on_unknown_command: Click 8 may raise UsageError before the group callback executes for unknown commands, meaning the test could pass vacuously whether or not the guard exists. The evidence is classified outcome: unknown -- no run confirmed the tautology -- so it is weighted as a strong opinion finding, not proof. The structural concern is architecturally sound and warrants a unit test that bypasses Click dispatch and exercises the guard directly with a mock context.

The remaining recommended findings are low-friction improvements suitable for this PR or an immediate same-day follow-up. python-architect's suggestion to replace cli.commands with ctx.command.commands is a zero-cost idiomatic Click swap that eliminates a theoretical stale-closure risk if the cli group is reconstructed in a test harness. devx-ux-expert correctly notes that bare apm now silently skips the update check -- a behavior change from before this PR -- and the inline comment should explicitly document this intent to prevent a future maintainer from reverting it as a bug. The growth-hacker's CHANGELOG framing ("Update notifications now appear only during recognized command invocations") is the right public-facing language.

Dissent. Three panelists flagged the inline comment from overlapping but non-contradictory angles: cli-logging-expert argues "fail fast" is inaccurate because the real motivation is output hygiene, not speed; python-architect objects that the comment conflates two semantically distinct skip cases (unknown command vs. bare invocation); devx-ux-expert flags that the bare-invocation skip is undocumented. All three readings are correct. python-architect's three-line split comment is the most informative structure, with cli-logging-expert's output-hygiene framing replacing "fail fast". The compound fix covers all three concerns in a single comment rewrite.

Aligned with: Pragmatic-as-npm -- update nudge preserved on all real workflow commands, no regression to the adoption funnel (P4 satisfied). Governed-by-policy -- CHANGELOG entry missing for a user-observable behavior change; trivially closeable in this PR (P6 open). OSS-community-driven -- external-contributor PR with no blocking concerns; move it (P7).

Growth signal. "Update notifications now appear only during recognized command invocations -- not on help display or mistyped commands" positions this as deliberate UX maturity rather than a bug fix. The bare apm help path -- often the first surface a new contributor encounters -- is now clean on first contact. That is a marginal but real improvement to the discovery funnel, and the CHANGELOG is the right place to signal it.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 1 Correct minimal guard clause preventing a spurious network call on unknown/bare commands; one recommended idiom improvement and one comment nit.
CLI Logging Expert 0 0 3 Output hygiene improvement is correct -- silencing the update banner before an error message is the right design; no findings block merge.
DevX UX Expert 0 1 1 Correct fail-fast behavior on invalid commands; the bare apm no-subcommand case silently also drops the update check -- defensible but undocumented.
Supply Chain Security Expert 0 0 2 Change is security-neutral to security-positive: narrows the update-check attack surface to recognized commands; no new vulnerabilities introduced.
OSS Growth Hacker 0 0 1 Net-positive UX fix: removes confusing update-nudge/error collisions on invalid input; real-work commands retain full nudge coverage, so upgrade conversion is unaffected.
Doc Writer 0 1 1 Fallback self-check fired: the Unreleased CHANGELOG block has no entry for this user-visible behavior fix, and the self-update docs may need precision.
Test Coverage Expert 0 1 2 Two of three new tests correctly defend real user promises at integration tier; the unknown-command test may be tautological if Click raises UsageError before the group callback fires.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] Add a unit test at tests/unit/test_cli_update_check_gate.py that constructs a mock Click context with ctx.invoked_subcommand='nonexistent' and directly exercises the guard, bypassing Click dispatch -- The integration test for the unknown-command case may pass vacuously if Click 8 raises UsageError before the group callback fires; a unit test makes the guard's correctness unambiguous and independent of Click routing internals.
  2. [Doc Writer] Add a ### Fixed entry to the [Unreleased] CHANGELOG block: "Update notifications now appear only during recognized command invocations -- not on help display or mistyped commands. (fix(cli): skip update check for unknown subcommands to fail fast #1541)" -- The [Unreleased] block is empty; this is a user-observable behavior change and P6 requires a changelog line.
  3. [Python Architect] Replace cli.commands with ctx.command.commands on line 146 of src/apm_cli/cli.py -- Zero-cost idiomatic Click swap; ctx.command.commands always refers to the live group object dispatching the current invocation, eliminating a stale-closure risk if the cli group is reconstructed in a future test harness.
  4. [DevX UX Expert] Update the inline comment on lines 145-146 to explicitly document that bare apm (help display) now skips the update check and that this is intentional -- Without an explicit comment, a future maintainer may treat the bare-invocation skip as a bug and revert it.
  5. [CLI Logging Expert] Tighten test assertions: replace assertIn('invalid', result.output) with assertIn("No such command 'invalid'", result.output), and add assertIn('Usage:', result.output) to the bare-invocation test -- The broad assertion passes on any output containing 'invalid'; the help-text assertion ensures a broken help path would not pass silently.

Architecture

classDiagram
    direction LR
    class cli_module {
        <<Module>>
    }
    class helpers_module {
        <<Module>>
    }
    class cli_group {
        <<ClickGroup>>
        +commands dict
        +callback(ctx, verbose) None
    }
    class ClickContext {
        <<ValueObject>>
        +resilient_parsing bool
        +invoked_subcommand str
        +command Group
    }
    class _check_and_notify_updates {
        <<Pure>>
        +call() None
    }
    class check_for_updates {
        <<IOBoundary>>
        +call(current_version str) str
    }
    cli_module *-- cli_group : defines
    helpers_module *-- _check_and_notify_updates : defines
    helpers_module *-- check_for_updates : defines
    cli_group ..> ClickContext : receives
    cli_group ..> _check_and_notify_updates : calls when guard passes
    _check_and_notify_updates ..> check_for_updates : delegates
    class cli_group:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    User(["apm CMD"]) --> GrpCB["cli group callback\ncli.py:128"]
    GrpCB --> G1{"ctx.resilient_parsing?"}
    G1 -- Yes --> SkipA["skip update check"]
    G1 -- No --> G2{"ctx.invoked_subcommand\nin cli.commands?\ncli.py:146"}
    G2 -- "No: None or unknown string" --> SkipB["skip update check"]
    G2 -- "Yes: known command" --> CANU["_check_and_notify_updates\n_helpers.py"]
    CANU --> G3{"is_self_update_enabled?\nAPM_E2E_TESTS set?"}
    G3 -- "disabled or E2E" --> SkipC["silent return"]
    G3 -- enabled --> NET["check_for_updates\nversion_checker.py"]
    NET -- "latest_version" --> Warn["print update notice\nclick.echo to stdout"]
    NET -- "None or raises" --> SkipC
    Warn --> Dispatch["Click: dispatch subcommand"]
    SkipA --> Dispatch
    SkipB --> Dispatch
    SkipC --> Dispatch
    Dispatch --> Known{"known command?"}
    Known -- Yes --> RunCmd["execute subcommand"]
    Known -- No --> ErrOut["UsageError: No such command X\nexit code 2"]
Loading
sequenceDiagram
    participant U as User
    participant CB as cli callback
    participant UPD as _check_and_notify_updates
    participant NET as GitHub API
    participant CK as Click dispatcher

    U->>CB: apm invalid
    Note over CB: invoked_subcommand = invalid
    Note over CB: GUARD: invalid not in cli.commands -- skip
    CB->>CK: update check bypassed
    CK-->>U: Error No such command invalid [exit 2]

    U->>CB: apm init test-project
    Note over CB: invoked_subcommand = init
    Note over CB: GUARD: init in cli.commands -- proceed
    CB->>UPD: _check_and_notify_updates()
    UPD->>NET: check_for_updates(current_version)
    NET-->>UPD: latest_version or None
    UPD-->>CB: returns
    CB->>CK: invoke init subcommand
    CK-->>U: init output [exit 0]
Loading

Recommendation

No panelist raised a blocking concern; zero findings at blocking severity. The core change is correct, aligned with pragmatic-as-npm conventions, and supported by all active specialists. The two highest-value pre-merge actions are the CHANGELOG entry (P6, trivial) and a unit test for the guard clause (test-coverage-expert structural concern). The ctx.command.commands idiom swap, comment rewrite incorporating output-hygiene framing, and test assertion tightening can land in this PR or an immediate follow-up -- they do not gate merge. This is an external-contributor PR and P7 applies: move it.


Full per-persona findings

Python Architect

  • [recommended] Use ctx.command.commands instead of cli.commands inside the group callback at src/apm_cli/cli.py:146
    cli.commands works correctly today because cli.add_command() calls at module load time populate the dict before any invocation. However, referencing the module-level name cli inside its own callback creates an implicit self-referential closure: if cli is reconstructed or aliased in a test harness, the guard silently reads stale membership from the original object. ctx.command.commands accesses the identical dict through the Click context, which always refers to the live group object dispatching the current invocation. This is the idiomatic Click pattern and is a zero-cost one-word swap.
    Suggested: if not ctx.resilient_parsing and ctx.invoked_subcommand in ctx.command.commands:

  • [nit] Comment conflates two semantically distinct skip reasons at src/apm_cli/cli.py:145
    The comment reads "skip on invalid input to fail fast." A bare apm invocation (ctx.invoked_subcommand is None) is not invalid input -- it is the canonical way to show help. Conflating it with unknown commands makes the intent harder to reason about in a future audit of this guard.
    Suggested: Split into three lines explaining unknown-command vs. bare-invocation separately.

CLI Logging Expert

  • [nit] Comment says "fail fast" but the motivation is output hygiene, not speed at src/apm_cli/cli.py:145
    The update check skip is not about making the error path faster -- the check is non-blocking and adds negligible latency. The real motivation is: printing an unrelated update banner before Click's error message is confusing noise.
    Suggested: # Check for updates only for known commands; skip on error/help paths to keep output clean.

  • [nit] test_no_update_check_on_unknown_command assertion is too broad at tests/integration/test_version_notification.py:80
    assertIn('invalid', result.output) passes as long as 'invalid' appears anywhere. A tighter assertion documents the Click error contract explicitly.
    Suggested: self.assertIn("No such command 'invalid'", result.output)

  • [nit] test_no_update_check_without_subcommand makes no assertion that help still renders at tests/integration/test_version_notification.py:97
    A broken help path (exception swallowed by CliRunner) would still pass.
    Suggested: Add: self.assertIn('Usage:', result.output)

DevX UX Expert

  • [recommended] Bare apm (no subcommand) now permanently skips the update check -- intent should be explicit at src/apm_cli/cli.py:146
    ctx.invoked_subcommand in cli.commands returns False for both an invalid command AND for None (bare apm). The bare-invocation case is a silent behavior change: a user who runs apm to browse help will never see an update banner. This is arguably the right call (npm, pip, cargo do not make network calls on bare help), but it should be explicitly acknowledged in the comment or PR description.
    Suggested: Update comment to explicitly acknowledge that bare apm also skips the update check and that this is intentional.

  • [nit] test_update_check_runs_on_valid_command missing exit-code assertion at tests/integration/test_version_notification.py:83
    If init fails silently in the isolated filesystem, the test still passes.
    Suggested: Add self.assertEqual(result.exit_code, 0, result.output) before the mock assertion.

Supply Chain Security Expert

  • [nit] Pre-existing: broad exception catch silently swallows TLS errors in the update check at src/apm_cli/commands/_helpers.py
    The bare except Exception: pass in _check_and_notify_updates() swallows TLS errors. This PR does not introduce or worsen this -- it reduces call frequency. A future issue to narrow exception scope would be appropriate.

  • [nit] Pre-existing: APM_REPO env var can redirect update-check URL to an arbitrary GitHub repo at src/apm_cli/utils/version_checker.py
    _get_air_gap_repo() reads APM_REPO from env. Impact is constrained by semver validation on the returned tag_name. This PR does not introduce or worsen this.

OSS Growth Hacker

  • [nit] CHANGELOG framing opportunity
    Frame this as a UX improvement: "Update notifications now appear only during real command invocations -- not on help display or mistyped commands." That framing signals APM's UX maturity and is shareable on its own merit.

Auth Expert -- inactive

PR #1541 modifies only the update-check gate condition in cli.py and its integration test -- no auth-related files are touched, and the change does not affect authentication behavior, token management, credential resolution, host classification, or remote-host fallback semantics.

Doc Writer

  • [recommended] Missing CHANGELOG entry for user-visible behavior change at CHANGELOG.md
    The [Unreleased] section is empty. This PR makes a user-observable behavioral change: the startup update notification is no longer emitted when an unknown or invalid command is invoked. Per the repo's Keep-a-Changelog discipline, all user-observable fixes belong in ### Fixed.
    Suggested: "- The startup update notification is no longer shown when an unknown command is invoked. APM now skips the version check on unrecognized input, so error output is not preceded by an unrelated notification. (fix(cli): skip update check for unknown subcommands to fail fast #1541)"

  • [nit] docs/src/content/docs/reference/cli/self-update.md wording is loose for the new gating condition
    The phrase "during normal command execution" is loose enough not to be wrong, but a reader consulting this section would benefit from precision about when the check is skipped.
    Suggested: "APM checks for new releases at most once per day when a recognized command runs. The check is skipped on unknown commands and bare invocations."

Test Coverage Expert

  • [recommended] test_no_update_check_on_unknown_command may be tautological: Click 8 can raise UsageError before group callback fires at tests/integration/test_version_notification.py:72
    For the guard to defend the unknown-command promise, Click must call the group callback WITH ctx.invoked_subcommand set to the unrecognised name BEFORE raising UsageError. Click 8's MultiCommand.invoke() may raise UsageError before the group callback executes. If so, both old and new code produce identical output and mock_check.assert_not_called() passes trivially in EITHER version. Test outcome: unknown (pytest not available in review env).
    Suggested: Add tests/unit/test_cli_update_check_gate.py that constructs a mock Click context with ctx.invoked_subcommand='nonexistent' and directly exercises the guard.
    Proof (unknown at integration-with-fixtures): tests/integration/test_version_notification.py::TestUpdateCheckSkippedOnInvalidCommand.test_no_update_check_on_unknown_command -- proves: An unknown CLI command does not trigger an update-check network call [devx]

  • [nit] test_no_update_check_without_subcommand does not document that bare-apm skipping update check is intentional at tests/integration/test_version_notification.py:92
    Before this PR, bare apm DID trigger the update check. A comment prevents a future maintainer from reverting it.
    Suggested: Add: # Intentional: bare 'apm' (help display) no longer pays the update-check network cost.

  • [nit] Patch target apm_cli.cli._check_and_notify_updates is correct -- patched at the import-site lookup in cli.py at tests/integration/test_version_notification.py:71
    Per tests.instructions.md, patch where the name is looked up. The new tests do this correctly. Positive observation; no change needed.

Performance Expert -- inactive

This PR touches only the CLI startup guard for invalid commands -- it does not change any dependency resolution, fetch, materialize, verify, cache, transport, or parallelism path.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

Generated by PR Review Panel for issue #1541 · sonnet46 11.8M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants