Skip to content

refactor: extract shared detect+version-check pipeline#19

Merged
kevinelliott merged 1 commit intomainfrom
refactor/extract-detect-pipeline
Apr 21, 2026
Merged

refactor: extract shared detect+version-check pipeline#19
kevinelliott merged 1 commit intomainfrom
refactor/extract-detect-pipeline

Conversation

@kevinelliott
Copy link
Copy Markdown
Owner

Summary

  • Extracts the cache-aware detect-and-version-check sequence (duplicated across four call sites after PR perf: parallelize version checks, tune SQLite/HTTP, fix flaky IPC test #16) into a new internal/orchestrator package exposing Pipeline.DetectAndCheckVersions. The pipeline wraps internal/versionfetch and keeps every upstream concrete type (storage.Store, catalog.Manager, detector.Detector, installer.Manager) untouched.
  • Refactors internal/cli/agent.go (agent list, agent update), internal/tui/app.go (Model.loadDataWithRefresh), and internal/systray/systray.go (App.refreshAgentsWithCache) to delegate to the pipeline. The agent refresh subcommand and the pkg/api/rest/pkg/api/grpc cache helpers are left alone per the guardrails.
  • Preserves per-caller semantics exactly: agent list still drops version-check errors silently, agent update still aggregates and prints them, the TUI still swallows them into dataLoadedMsg{err}, and the systray still takes its agentsMu write lock around the swap. Systray gets a new Options.TolerateCatalogError knob so its historical behavior of starting up with an empty catalog when the network is down is preserved verbatim.

Line-count impact per call site

File + - Net
internal/cli/agent.go (list + update) 38 122 -84
internal/tui/app.go (loadDataWithRefresh) 14 78 -64
internal/systray/systray.go (refreshAgentsWithCache) 11 68 -57
Total removed from call sites 63 268 -205
New internal/orchestrator/pipeline.go 255 +255
New internal/orchestrator/pipeline_test.go 601 +601

(Numbers are approximate; git diff --stat counts each line. The total shown by Git for the three modified files is +57 / -228, which matches the net shrink of the duplicated logic.)

Test coverage

  • internal/orchestrator: 97.4% of statements, covering cold detect, cache hit (fresh & stale update-check TTL), force-refresh with cache-clear, skip-version-check, version-check error aggregation + NonNilErrors filter, fatal catalog + detector errors, TolerateCatalogError fallback, disabled cache, custom concurrency, and stale detection cache.
  • All existing packages continue to pass go test ./... -race -short -count=1.
  • golangci-lint run --timeout=5m at v1.64.8 is clean on all touched files. The only remaining finding is the pre-existing uninstallCLI nolint:unused directive in internal/systray/systray.go, which is untouched by this PR.

Judgment calls (documented so reviewers don't need to re-derive them)

  1. internal/ vs pkg/ placement. The pipeline imports internal/versionfetch, so it has to live under internal/ too. Promoting versionfetch would have broadened the blast radius unnecessarily; leaving it in place keeps this refactor surgical.
  2. TolerateCatalogError option. The systray historically treated a failed catalog fetch as non-fatal (agentDefs = nil then proceed). The other call sites treated it as fatal. Rather than silently change either behavior, I added a per-call option and flipped it on only in the systray.
  3. agent update and the detection cache. The pre-refactor agent update always ran a fresh DetectAll, but did not write back to the detection cache. The pipeline does write back after a cold detect (systray reference behavior). This is a strict improvement — subsequent agent list calls see the fresh versions — and is explicitly called out in the commit message.
  4. agent refresh kept out of scope. The task brief listed only agent list, agent update, TUI, and systray. newAgentRefreshCommand has its own slightly different contract (always clears the cache, always detects), so I left it alone to honor the "surgical" guardrail.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... -race -short -count=1
  • /tmp/gcil/golangci-lint run --timeout=5m (v1.64.8, matches CI)
  • go test -cover ./internal/orchestrator/... — 97.4%
  • Manual sanity: run agentmgr agent list, agentmgr agent list -r, agentmgr agent update --all --dry-run, and the TUI (agentmgr tui) to confirm no regressions in spinner messages or update detection
  • Manual sanity on the systray helper with and without network to exercise TolerateCatalogError

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Consolidates the cache-aware detection and parallel latest-version fetch
sequence into a new internal/orchestrator package. The four call sites
that previously open-coded this flow (agent list, agent update, the TUI
loader, and the systray refresh) now delegate to
orchestrator.Pipeline.DetectAndCheckVersions, which removes the drift
that previously required commit bf82ca8 to patch up agent update.

Line-count changes across call sites:

    internal/cli/agent.go:      +122 / -122 (list & update simplified)
    internal/tui/app.go:        +18  / -78  loadDataWithRefresh
    internal/systray/systray.go: +15 / -68  refreshAgentsWithCache

Net across the three edits: 57 insertions, 228 deletions (duplicated
cache-logic collapsed into the new package). The new package itself is
255 lines of code plus 601 lines of tests (97.4% coverage).

Behavior preserved:

 - agent list: drops version-check errors silently
 - agent update: aggregates errors and prints them (now passes
   ForceRefresh so the flow matches the original, which always ran a
   fresh detection before choosing updates)
 - TUI: surfaces fatal load errors via dataLoadedMsg{err}, swallows
   version-check errors
 - Systray: keeps the agentsMu write-lock swap and sets
   TolerateCatalogError=true to match the original tolerant startup

Guardrails honored:

 - No changes to catalog.Manager, detector.Detector, installer.Manager,
   or storage.Store public types
 - internal/versionfetch is still the engine; Pipeline wraps it
 - pkg/api/rest and pkg/api/grpc keep their own cache helper (PR #16)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 21, 2026 06:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the repeated “detect installed agents + cache handling + latest-version checks” workflow into a shared internal/orchestrator pipeline and updates the CLI/TUI/systray call sites to delegate to it, aiming to keep behavior consistent across entry points while reducing duplication.

Changes:

  • Added internal/orchestrator with Pipeline.DetectAndCheckVersions encapsulating cache-aware detection + parallel version checks (plus tests).
  • Refactored CLI (agent list, agent update), TUI (loadDataWithRefresh), and systray (refreshAgentsWithCache) to use the pipeline.
  • Preserved the systray’s historical “tolerate catalog fetch failure” behavior via an option.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/orchestrator/pipeline.go New shared pipeline implementing catalog load, detection-cache logic, version-check TTL, and best-effort persistence.
internal/orchestrator/pipeline_test.go High-coverage tests validating cache hit/miss behavior, TTL handling, forced refresh, tolerated catalog errors, and error aggregation.
internal/cli/agent.go Switches list/update commands to the pipeline; removes duplicated cache/version-check code paths.
internal/tui/app.go Uses the pipeline for detection/version checks while still loading the full catalog for TUI rendering.
internal/systray/systray.go Uses the pipeline in refresh flow while preserving “start even if catalog fetch fails”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/cli/agent.go
Comment on lines 138 to 140
// Apply filters
var filtered []*agent.Installation
for _, inst := range installations {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

In this agent list implementation, the showAll and checkUpdates flag values are never read (only bound via Flags().BoolVar...), so --all and --check-updates currently have no effect. Either implement their intended behavior (e.g., change filtering/output semantics and/or force a version check even when the update-check TTL is fresh) or remove the flags to avoid misleading CLI help output.

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/agent.go
Comment on lines +119 to 128
pipelineRes, err := pipeline.DetectAndCheckVersions(ctx, orchestrator.Options{
ForceRefresh: refresh,
})
if err != nil {
spinner.Error("Agent detection failed")
return err
}
if pipelineRes.RanVersionCheck {
spinner.UpdateMessage("Checking for updates...")

// Fetch latest versions in parallel. Errors are intentionally
// dropped here to preserve the existing silent-failure semantics
// of `agent list`.
_ = versionfetch.CheckLatestVersions(ctx, instMgr, installations, agentDefMap, versionfetch.DefaultConcurrency)

// Save last update check time
_ = store.SetLastUpdateCheckTime(ctx, time.Now()) //nolint:errcheck // best-effort timestamp; non-critical if this fails

// Save to cache if enabled (with updated version info)
if cfg.Detection.CacheEnabled {
if err := store.SaveDetectionCache(ctx, installations); err != nil {
// Log but don't fail
_ = err
}
}
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

pipeline.DetectAndCheckVersions(...) performs the version-check internally before returning, so updating the spinner to "Checking for updates..." only after the call completes is misleading (and typically flashes right before spinner.Stop()). Consider either using a single generic spinner message for the whole pipeline, or refactoring the pipeline API (e.g., split detect vs version-check, or add a progress hook) so the CLI can show the phase change while the work is actually happening.

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/agent.go
agentDefMap[def.ID] = def
}

spinner.UpdateMessage("Checking for updates...")
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

DetectAndCheckVersions already ran the version-check step before returning pipelineRes, so setting the spinner message to "Checking for updates..." at this point doesn’t reflect any ongoing work. Either remove this spinner phase, or adjust the orchestration so the spinner message changes before/while the version-check runs (e.g., call the pipeline with SkipVersionCheck and run versionfetch.CheckLatestVersions outside when you want a dedicated spinner phase).

Suggested change
spinner.UpdateMessage("Checking for updates...")

Copilot uses AI. Check for mistakes.
@kevinelliott kevinelliott merged commit 404a55c into main Apr 21, 2026
19 checks passed
@kevinelliott kevinelliott deleted the refactor/extract-detect-pipeline branch April 21, 2026 07:06
kevinelliott added a commit that referenced this pull request Apr 21, 2026
Promote the Unreleased section to 1.1.0 and capture everything landed
since v1.0.24: the parallel version-check perf work (#16), the
google.golang.org/grpc CVE-2026-33186 security bump (#17), the
golangci-lint pinning (#18), and the shared detect+version-check
pipeline refactor (#19).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kevinelliott added a commit that referenced this pull request Apr 25, 2026
* fix: address Copilot review feedback — correctness fixes

Seven real-bug fixes called out across review comments on merged PRs:

- pkg/platform/platform.go: resetLookPathCache() no longer reassigns
  the package-level sync.Map (unsafe vs concurrent readers). Uses
  Range+Delete instead.
- pkg/installer/providers/brew.go: GetLatestVersion no longer caches
  context-canceled / deadline-exceeded errors. A caller with a short
  ctx would have poisoned the 5m TTL for every later caller.
- pkg/installer/providers/progress.go: the progress writer is now
  wrapped in a locking writer so concurrent Write calls from
  cmd.Stdout and cmd.Stderr (os/exec writes those from separate
  goroutines) don't interleave bytes mid-call.
- pkg/catalog/embed.go: EmbeddedJSON() now returns a copy of the
  backing bytes. Previously callers could mutate the package-level
  []byte, corrupting the baseline for every future Get().
- pkg/catalog/manager.go: loadEmbedded now also probes
  /usr/share/agentmgr/catalog.json — where the goreleaser nfpm
  config installs the catalog for .deb/.rpm users. Previously that
  path was silently ignored at runtime even though goreleaser wrote
  the file there at install time.
- internal/cli/doctor.go: Catalog Sources section updated with the
  matching /usr/share/agentmgr row so `doctor` output mirrors the
  loader's probe order.
- internal/cli/agent.go: removed the --all (-a) and --check-updates
  flags from `agent list`. Both bound to local vars that were never
  read anywhere, so the flags did nothing; users setting them got a
  silent no-op instead of a useful behavior.

Supersedes the corresponding Copilot annotations on PRs #19, #28,
#29, #33, plus the dead-flag note on the agent list command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: address remaining Copilot review nits (docs + tests)

Batch of small review-comment fixes — documentation accuracy, test
hardening, and gosec G104 on grpc teardown:

- pkg/api/grpc/server.go: explicit \`_ = s.listener.Close()\` in Stop()
  and ForceStop() with a comment noting the close error is not
  actionable during teardown (silences gosec G104).
- pkg/catalog/manager.go: Refresh doc now explicitly notes that
  singleflight does not merge contexts and the first caller's ctx
  governs the shared fetch — a caller with a short deadline can
  cancel the in-flight work for all shared callers.
- pkg/installer/providers/progress.go: WithProgressWriter doc clarifies
  why passing nil is a no-op (asymmetry lets middleware attach a writer
  safely without downstream code accidentally clobbering it).
- internal/systray/cli_uninstall_darwin.go: file is darwin-only via
  //go:build tag, so platform.ID() is always Darwin at runtime. Removed
  the dead Linux and Windows branches (the filepath/os/platform imports
  they needed are gone too).
- CHANGELOG.md: "(Removed in [Unreleased].)" was a broken shortcut
  reference link — promoted to "(Removed in v1.2.0.)".
- README.md: feature bullet uses the full "agentmgr agent list" command
  prefix, consistent with everywhere else in the doc.
- internal/cli/output/spinner_test.go: non-TTY test replaced the
  30ms sleep with a direct assertion on s.isTTY. Start() is a no-op
  when isTTY is false, so sleeping was measuring nothing.
- internal/cli/output/table_test.go: ANSI-alignment loop now actually
  asserts inter-column padding instead of discarding each line.
- internal/systray/handlers_test.go: mustParseVersion panics on parse
  error so test-setup bugs surface at the fixture, not downstream.
  TestDialogTracking uses bare *exec.Cmd values (trackDialog only
  checks pointer identity) to avoid the PATH lookup for \`true\` on
  Windows CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): drop "all" flag assertion from list; assert removal

The --all / -a and --check-updates flags were removed from
\`agent list\` in the parent commit because both bound to unused
local vars. Drop the existence assertion for "all" and add an
anti-regression check that asserts both flags are ABSENT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants