Skip to content

feat: complete agent self-upgrade closure#97

Merged
ZingerLittleBee merged 17 commits intomainfrom
feat/agent-self-upgrade-closure
Apr 14, 2026
Merged

feat: complete agent self-upgrade closure#97
ZingerLittleBee merged 17 commits intomainfrom
feat/agent-self-upgrade-closure

Conversation

@ZingerLittleBee
Copy link
Copy Markdown
Owner

@ZingerLittleBee ZingerLittleBee commented Apr 14, 2026

Summary

  • add the agent self-upgrade backend flow, including protocol updates, release lookup, job tracking, timeout cleanup, and browser websocket hydration
  • wire agent progress and reconnect success handling back into server state and surface upgrade status in the server detail, list, and card UI
  • add docs and manual QA coverage for the upgrade flow, and fix frontend test mocks so the full web suite stays green

Test Plan

  • cargo test --workspace
  • bun run test
  • bun run typecheck

Summary by CodeRabbit

  • New Features

    • Agent self-upgrade UI: show current vs. latest version, upgrade availability, and start upgrade (admin-only).
    • Real-time staged progress and status badges (downloading, verifying, pre-flight, installing, restarting); success/failure/timeout handling.
    • Server list and detail pages display upgrade badges and a dedicated version/upgrade section.
    • Public endpoint to fetch latest agent version; configurable latest-version URL.
  • Documentation

    • Configuration docs and a QA checklist for agent upgrade flows.
  • Localization

    • English and Chinese UI strings for upgrade flows.

ZingerLittleBee and others added 16 commits April 14, 2026 20:01
Closes the three gaps in the existing agent self-upgrade flow:
- Result/progress reporting from agent to server (new AgentMessage variants)
- Frontend UI in CapabilitiesDialog + server list badge
- Pre-flight validation + .bak.<timestamp> retention for rollback safety
Addresses 6 findings and 2 open questions:
- Add job_id (UUID v4) as primary matching key to prevent stale messages from
  polluting same-version retries; keep target_version as compat fallback.
- Move start_job after all fallible pre-checks so agent-offline / platform-unsupported
  / checksum-fetch failures no longer create phantom Running jobs.
- Extend AgentInfoUpdated to broadcast agent_version; front-end react-query cache
  now sees live Current version after upgrade completes.
- Extend FullSync payload with upgrades array so WS reconnect / fresh page loads
  hydrate list badges correctly.
- Latest-version endpoint now auto-detects GitHub Releases and supports
  latest_version_url override for self-hosted installs.
- CapabilityDenied(upgrade) immediately fails the active job (no 120s wait).
- Relocate AgentVersionSection from admin-only CapabilitiesDialog to Server detail
  page; Members get read-only visibility, admins keep Upgrade/Retry buttons.
- Correct i18n stance: apps/web uses react-i18next; add en/zh key table.
Nine tasks covering protocol extension, server tracker + timeout worker,
latest-version lookup service, REST/WS wiring with start_job ordering,
agent hardening with preflight and timestamped .bak rollback, server
integration coverage, frontend Zustand store + hooks + WS hydration,
server detail UI + list badge, and docs/QA sweep.

Includes four review-driven amendments folded in up front:
- Preserve CAP_UPGRADE pre-check in trigger_upgrade
- Fold CapabilityDenied upgrade handling into existing match arm
- Convert handleWsMessage from private to exported in place
- Add Verifying-failure and timeout-sweep integration tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
server-bee-docs Ready Ready Preview, Comment Apr 14, 2026 1:31pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 040bbba1-f393-48b8-a810-a714af46a46c

📥 Commits

Reviewing files that changed from the base of the PR and between a2f4fdb and 7080233.

📒 Files selected for processing (8)
  • apps/web/src/components/server/agent-version-section.test.tsx
  • apps/web/src/components/server/upgrade-job-badge.tsx
  • apps/web/src/components/uptime/uptime-timeline.test.tsx
  • apps/web/src/hooks/use-servers-ws.ts
  • apps/web/src/routes/_authed/servers/$id.test.tsx
  • apps/web/src/routes/_authed/servers/$id.tsx
  • apps/web/src/stores/upgrade-jobs-store.test.ts
  • crates/server/src/service/upgrade_tracker.rs
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/stores/upgrade-jobs-store.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/src/routes/_authed/servers/$id.test.tsx
  • apps/web/src/routes/_authed/servers/$id.tsx
  • apps/web/src/components/uptime/uptime-timeline.test.tsx
  • apps/web/src/components/server/upgrade-job-badge.tsx
  • apps/web/src/hooks/use-servers-ws.ts
  • apps/web/src/components/server/agent-version-section.test.tsx
  • crates/server/src/service/upgrade_tracker.rs

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: complete agent self-upgrade closure' directly and clearly summarizes the main change—implementing the complete end-to-end agent self-upgrade workflow across backend, frontend, and protocol layers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-self-upgrade-closure

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/agent/src/reporter.rs (1)

1935-1945: ⚠️ Potential issue | 🟡 Minor

Missing PreFlight stage emission before Installing.

The protocol defines five upgrade stages (Downloading, Verifying, PreFlight, Installing, Restarting), but this implementation skips PreFlight. According to the PR documentation and i18n strings, PreFlight represents pre-installation checks (e.g., backup creation). Consider emitting it after writing the temp file and before the backup/replace operation.

Proposed fix to add PreFlight stage
     #[cfg(unix)]
     {
         use std::os::unix::fs::PermissionsExt;
         std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o755))?;
     }

+    emit_upgrade_progress(&tx, job_id.clone(), version, UpgradeStage::PreFlight).await;
+
     emit_upgrade_progress(&tx, job_id.clone(), version, UpgradeStage::Installing).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/agent/src/reporter.rs` around lines 1935 - 1945, The code skips
emitting the PreFlight stage; add an emit_upgrade_progress call with
UpgradeStage::PreFlight after the temp file has been written and before the
backup/replace block so pre-installation checks are reported. Specifically, call
emit_upgrade_progress(&tx, job_id.clone(), version,
UpgradeStage::PreFlight).await; placed between the temp file write/flush step
and the backup/replace operations that use std::fs::remove_file/rename, using
job_id.clone() to avoid moving job_id prematurely.
🧹 Nitpick comments (2)
apps/web/src/components/server/server-card.tsx (1)

172-172: Keep the store subscription scoped to the badge.

Line 172 subscribes the entire ServerCardInner to upgrade-job updates, so every progress tick will re-render the whole memoized card, including charts and traffic/network work. Please extract this into a tiny badge-only child, like the UpgradeBadgeCell pattern already used in apps/web/src/routes/_authed/servers/index.tsx.

♻️ Suggested extraction
+function ServerCardUpgradeBadge({ serverId }: { serverId: string }) {
+  const upgradeJob = useUpgradeJobsStore((state) => state.jobs.get(serverId))
+  return <UpgradeJobBadge job={upgradeJob} />
+}
+
 const ServerCardInner = ({ server }: ServerCardProps) => {
   const { t } = useTranslation(['servers'])
   const { data: networkOverview = [] } = useNetworkOverview()
   const { data: realtimeData } = useNetworkRealtime(server.id)
   const { data: trafficOverview } = useTrafficOverview()
-  const upgradeJob = useUpgradeJobsStore((state) => state.jobs.get(server.id))
 
   // ...
 
         <div className="flex items-center gap-1.5">
-          <UpgradeJobBadge job={upgradeJob} />
+          <ServerCardUpgradeBadge serverId={server.id} />
           <StatusBadge online={server.online} />
         </div>

Please sanity-check this with React Profiler while an upgrade is streaming progress in grid view.

Also applies to: 219-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/server/server-card.tsx` at line 172, ServerCardInner
currently calls useUpgradeJobsStore((state) => state.jobs.get(server.id)) which
subscribes the whole memoized card to upgrade-job updates; extract that
subscription into a tiny child component (e.g., UpgradeBadge or reuse the
existing UpgradeBadgeCell pattern) that receives server.id as a prop and calls
useUpgradeJobsStore there to read only the job for that id, then render the
badge from that child inside ServerCardInner and remove the useUpgradeJobsStore
call from ServerCardInner (also fix similar subscriptions around the code
referenced at the original lines 219-222); after change, sanity-check with React
Profiler to ensure only the badge re-renders on progress ticks.
docs/superpowers/plans/2026-04-14-agent-self-upgrade-closure.md (1)

2027-2028: Note: Actual implementation differs from plan's store interface.

The plan specifies upsertJob method but the actual implementation in upgrade-jobs-store.ts uses setJob instead. The plan's jobs type is Record<string, UpgradeJobDto> but the implementation uses Map<string, UpgradeJob>. This is acceptable as implementation details evolved, but worth noting for future plan accuracy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-14-agent-self-upgrade-closure.md` around lines
2027 - 2028, The plan's API and types are out-of-sync with the implementation:
replace references to upsertJob and Record<string, UpgradeJobDto> in the plan
with the actual implementation symbols (setJob and Map<string, UpgradeJob>) and
update any examples like state.jobs[job.server_id] to the Map access pattern
(e.g., jobs.get(job.server_id)) or otherwise document that the store uses a Map;
alternatively, if you prefer the plan to drive code, change the implementation
in upgrade-jobs-store.ts to expose upsertJob and use a plain Record<string,
UpgradeJobDto> so names and types match—pick one approach and make the plan and
implementation consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/dashboard/widget-config-dialog.test.tsx`:
- Around line 74-89: The Checkbox mock's onChange currently toggles using
!checked which can drift from real DOM behavior; update the mock component
(Checkbox in the test file) to read the new checked state from the event (use
event.currentTarget.checked) inside the onChange handler and pass that value to
onCheckedChange so the test simulates actual input state transitions
consistently.

In `@apps/web/src/components/server/upgrade-job-badge.tsx`:
- Around line 32-35: Update the Tooltip usage to match `@base-ui/react/tooltip`'s
prop API: in the TooltipProvider component replace the invalid prop
delayDuration with delay (i.e., TooltipProvider delay={100}), and remove the
unsupported asChild prop from TooltipTrigger (use TooltipTrigger directly
wrapping the Badge instead). Keep Tooltip, TooltipTrigger, Badge and
config.variant as they are; only change the prop name on TooltipProvider and
remove asChild from TooltipTrigger.

In `@apps/web/src/components/uptime/uptime-timeline.test.tsx`:
- Around line 7-29: The test uses vi.mock(...) but never imports vi, causing
TS2304; update the test module's imports to include vi from 'vitest' (add vi to
the existing import statement or add a new import) so that the vi.mock call in
uptime-timeline.test.tsx resolves and the TypeScript typecheck passes.

In `@apps/web/src/hooks/use-servers-ws.ts`:
- Around line 351-374: TypeScript isn't narrowing `raw` in the
'upgrade_progress' branch, so replace the ad-hoc typeof checks + cast with a
user-defined type guard; add a function like isUpgradeProgressPayload(x): x is {
server_id: string; job_id: string; target_version: string; stage: string } and
call if (!isUpgradeProgressPayload(raw)) break; then safely destructure const {
server_id, job_id, target_version, stage } = raw and continue to call
useUpgradeJobsStore.getState().getJob(server_id) and setJob(...) using
UpgradeJob['stage'] as needed.
- Around line 376-407: The TypeScript error comes from casting raw to a complex
type inside the 'upgrade_result' case; after your runtime typeof checks, stop
using the inline "as { ... }" cast and instead build a properly typed payload
object from the validated raw fields (e.g. create const payload = { server_id:
raw.server_id, job_id: raw.job_id, target_version: raw.target_version, status:
raw.status, stage: raw.stage, error: raw.error ?? null, backup_path:
raw.backup_path ?? null } and then narrow stage/status into UpgradeJob['stage']
and UpgradeJob['status'] as needed), then call
useUpgradeJobsStore.getState().setJob(server_id, { ...payload, stage:
payload.stage ?? existingJob?.stage ?? 'downloading', started_at:
existingJob?.started_at ?? now, finished_at: now }); this avoids the failing
cast while preserving your runtime checks and uses the existing functions getJob
and setJob and the UpgradeJob types.

In `@apps/web/src/routes/_authed/servers/`$id.tsx:
- Around line 527-528: The useUpgradeJobsStore hook (used to derive upgradeJob)
is called after early returns (serverLoading and !server), violating Rules of
Hooks; move the useUpgradeJobsStore((state) => state.jobs.get(id)) call up to
the top of the component with the other hooks (e.g., immediately after existing
useState/useEffect or near line ~365), so it always runs on every render, then
remove the late call at the bottom (the current upgradeJob assignment) and use
the moved upgradeJob variable in the JSX/logic that follows the early returns.

In `@apps/web/src/routes/_authed/servers/index.tsx`:
- Around line 140-143: The column with id 'upgrade' currently sets header to
null which hides the label from screen readers; update the column definition
(the header property on the column with id 'upgrade' that renders
UpgradeBadgeCell in cell) to return a non-visual/visually-hidden label (e.g., a
span with an "sr-only" class or your app's visuallyHidden helper) containing the
text "Upgrade" so the column is announced to assistive tech while keeping the
visual compactness.

In `@apps/web/src/stores/upgrade-jobs-store.test.ts`:
- Around line 15-40: The helper makeJob is widening literal types to plain
string causing incompatible calls; change its type signature to use the real
model (e.g., makeJob(overrides: Partial<UpgradeJob> = {}): UpgradeJob) so the
returned object and the overrides preserve UpgradeStage and UpgradeStatus unions
instead of string; update the function return type to UpgradeJob and ensure
overrides is Partial<UpgradeJob> (or at minimum type stage/status as
UpgradeStage/UpgradeStatus) so calls to setJob and setJobs accept the result
without type errors.

In `@apps/web/src/stores/upgrade-jobs-store.ts`:
- Around line 29-31: The isFinished function currently only treats 'succeeded'
and 'failed' as terminal states; include 'timeout' so timeout jobs auto-clear.
Update the isFinished(status: UpgradeStatus) implementation (function
isFinished) to return true for status === 'succeeded' || status === 'failed' ||
status === 'timeout', ensuring UpgradeStatus 'timeout' is recognized as a
finished state and will be auto-cleared like other terminal states.

In `@crates/agent/src/reporter.rs`:
- Around line 1952-1963: The failure emission currently passes job_id: None when
cmd.spawn() fails because job_id was previously moved; locate the code around
emit_upgrade_progress and the cmd.spawn() block and preserve the job id by
cloning it before it is consumed (or change ownership to borrow) so you can pass
Some(job_id.clone()) into emit_upgrade_failure(&tx, Some(job_id.clone()),
version.to_string(), UpgradeStage::Restarting, error.to_string(),
Some(backup_path.display().to_string())). Ensure you clone the original job_id
variable right before the call that consumes it (the emit_upgrade_progress call
at/near where job_id is currently moved) so the spawn failure path has the real
job id to report.

In `@crates/server/src/router/api/server.rs`:
- Around line 571-589: Before creating a job, perform preflight checks: after
getting the sender via state.agent_manager.get_sender(&id) but before calling
state.upgrade_release_service.resolve_asset(...) and
state.upgrade_tracker.start_job(...), read the agent's current runtime/system
info (e.g., from the returned sender or agent record) to (1) detect a no-op
upgrade (agent already at the requested version) and return an immediate
success/Idempotent response, and (2) verify the agent's effective capabilities
include CAP_UPGRADE and return a suitable error (e.g., AppError::Forbidden or
AppError::Conflict) if missing; implement these checks using the existing
symbols (state.agent_manager.get_sender, CAP_UPGRADE,
state.upgrade_release_service.resolve_asset, state.upgrade_tracker.start_job,
StartUpgradeJobError::Conflict) so the code fails fast before fetching assets or
creating jobs.

In `@crates/server/src/service/upgrade_release.rs`:
- Around line 84-88: The code in upgrade_release.rs uses let-chain syntax (the
if let Some(cached) = cache.as_ref() && !cached.is_expired() { return
cached.response(); }) which requires Rust 1.88+/edition 2024; update the project
MSRV by adding rust-version = "1.88" to Cargo.toml (or document the
requirement), or alternatively rewrite the branch to avoid let-chains (e.g.,
match or nested ifs) if you cannot raise the MSRV; reference the let-chain line
around cache.as_ref(), cached.is_expired(), and cached.response() when making
the change.

In `@tests/agent-upgrade.md`:
- Around line 124-173: The checklist references WS messages and a GET endpoint
that don't exist; update the test steps and API surface to match the actual
upgrade flow driven by full_sync.upgrades, upgrade_progress, and upgrade_result
and the router that only exposes POST /api/servers/{id}/upgrade; remove or
replace references to CapabilitiesChanged/ServerUpdate WebSocket messages and
/api/servers/:id/upgrade-status, and instead instruct QA to observe the
full_sync.upgrades event stream and the upgrade_progress/upgrade_result payloads
(or the single POST call response and subsequent state changes) when validating
behavior.
- Around line 91-101: The test scenario should be changed to verify same-server
concurrent upgrade rejection because UpgradeJobTracker conflicts on server_id;
update the steps and expectations in the "Verify Concurrent Upgrade Rejection"
section to start an upgrade on a single server (e.g., Server A), then attempt to
start another upgrade on that same server (or rapidly click the upgrade button
on that same server) and assert UpgradeJobTracker rejects the second attempt
with the "Upgrade already in progress" message and the UI prevents initiating a
second upgrade while the first continues; ensure all mentions of "server B" or
different-server attempts are removed and reference UpgradeJobTracker and
server_id as the concurrency key in the scenario description.

---

Outside diff comments:
In `@crates/agent/src/reporter.rs`:
- Around line 1935-1945: The code skips emitting the PreFlight stage; add an
emit_upgrade_progress call with UpgradeStage::PreFlight after the temp file has
been written and before the backup/replace block so pre-installation checks are
reported. Specifically, call emit_upgrade_progress(&tx, job_id.clone(), version,
UpgradeStage::PreFlight).await; placed between the temp file write/flush step
and the backup/replace operations that use std::fs::remove_file/rename, using
job_id.clone() to avoid moving job_id prematurely.

---

Nitpick comments:
In `@apps/web/src/components/server/server-card.tsx`:
- Line 172: ServerCardInner currently calls useUpgradeJobsStore((state) =>
state.jobs.get(server.id)) which subscribes the whole memoized card to
upgrade-job updates; extract that subscription into a tiny child component
(e.g., UpgradeBadge or reuse the existing UpgradeBadgeCell pattern) that
receives server.id as a prop and calls useUpgradeJobsStore there to read only
the job for that id, then render the badge from that child inside
ServerCardInner and remove the useUpgradeJobsStore call from ServerCardInner
(also fix similar subscriptions around the code referenced at the original lines
219-222); after change, sanity-check with React Profiler to ensure only the
badge re-renders on progress ticks.

In `@docs/superpowers/plans/2026-04-14-agent-self-upgrade-closure.md`:
- Around line 2027-2028: The plan's API and types are out-of-sync with the
implementation: replace references to upsertJob and Record<string,
UpgradeJobDto> in the plan with the actual implementation symbols (setJob and
Map<string, UpgradeJob>) and update any examples like state.jobs[job.server_id]
to the Map access pattern (e.g., jobs.get(job.server_id)) or otherwise document
that the store uses a Map; alternatively, if you prefer the plan to drive code,
change the implementation in upgrade-jobs-store.ts to expose upsertJob and use a
plain Record<string, UpgradeJobDto> so names and types match—pick one approach
and make the plan and implementation consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 852159b3-a4ba-4c59-8477-115c746274b0

📥 Commits

Reviewing files that changed from the base of the PR and between d551b11 and a2f4fdb.

📒 Files selected for processing (41)
  • ENV.md
  • apps/docs/content/docs/cn/configuration.mdx
  • apps/docs/content/docs/en/configuration.mdx
  • apps/web/src/components/dashboard/widget-config-dialog.test.tsx
  • apps/web/src/components/server/agent-version-section.test.tsx
  • apps/web/src/components/server/agent-version-section.tsx
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/components/server/traffic-card.test.tsx
  • apps/web/src/components/server/upgrade-job-badge.tsx
  • apps/web/src/components/uptime/uptime-timeline.test.tsx
  • apps/web/src/hooks/use-servers-ws.test.ts
  • apps/web/src/hooks/use-servers-ws.ts
  • apps/web/src/hooks/use-upgrade-job.test.ts
  • apps/web/src/hooks/use-upgrade-job.ts
  • apps/web/src/locales/en/servers.json
  • apps/web/src/locales/zh/servers.json
  • apps/web/src/routes/_authed/servers/$id.test.tsx
  • apps/web/src/routes/_authed/servers/$id.tsx
  • apps/web/src/routes/_authed/servers/index.tsx
  • apps/web/src/stores/upgrade-jobs-store.test.ts
  • apps/web/src/stores/upgrade-jobs-store.ts
  • crates/agent/src/reporter.rs
  • crates/common/src/protocol.rs
  • crates/server/src/config.rs
  • crates/server/src/main.rs
  • crates/server/src/openapi.rs
  • crates/server/src/router/api/agent.rs
  • crates/server/src/router/api/mod.rs
  • crates/server/src/router/api/server.rs
  • crates/server/src/router/ws/agent.rs
  • crates/server/src/router/ws/browser.rs
  • crates/server/src/service/mod.rs
  • crates/server/src/service/upgrade_release.rs
  • crates/server/src/service/upgrade_tracker.rs
  • crates/server/src/state.rs
  • crates/server/src/task/mod.rs
  • crates/server/src/task/upgrade_timeout.rs
  • docs/superpowers/plans/2026-04-14-agent-self-upgrade-closure.md
  • docs/superpowers/specs/2026-04-14-agent-self-upgrade-closure-design.md
  • tests/README.md
  • tests/agent-upgrade.md

Comment on lines +74 to +89
Checkbox: ({
checked,
onCheckedChange,
...props
}: {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
} & Record<string, unknown>) => (
<input
checked={checked}
data-testid="checkbox"
onChange={() => onCheckedChange?.(!checked)}
type="checkbox"
{...props}
/>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether tri-state checkbox semantics are used in the repo.
rg -n -C3 "onCheckedChange|indeterminate|checked\s*:\s*'indeterminate'" --type=ts --type=tsx

Repository: ZingerLittleBee/ServerBee

Length of output: 96


🌐 Web query:

For the @base-ui/react/checkboxversion used by this repository, what are the exact types and semantics for Checkbox.RootcheckedandonCheckedChange?

💡 Result:

For @base-ui/react/checkbox Checkbox.Root: checked: boolean | undefined (controlled checked state; use defaultChecked for uncontrolled) onCheckedChange: ((checked: boolean, eventDetails: CheckboxRootChangeEventDetails) => void) | undefined Where CheckboxRootChangeEventDetails = BaseUIChangeEventDetails with reason: 'none'. Note: The query mentions "version used by this repository" but no specific repo provided; types from @base-ui/react@1.1.0 and current docs match. Base UI (MUI) follows similar patterns to Radix but uses boolean (not 'indeterminate') for checked prop.

Citations:


🏁 Script executed:

#!/bin/bash
# Retry rg search with correct file types (use ts instead of tsx, or use generic search)
rg -n "onCheckedChange|indeterminate" --type ts apps/web/src

Repository: ZingerLittleBee/ServerBee

Length of output: 3601


🏁 Script executed:

#!/bin/bash
# Check package.json for `@base-ui/react` version
cat apps/web/package.json | grep -A2 -B2 "@base-ui/react"

Repository: ZingerLittleBee/ServerBee

Length of output: 207


🏁 Script executed:

#!/bin/bash
# Find and inspect the production checkbox component
fd -e ts -e tsx "checkbox" apps/web/src/components/ui/ 2>/dev/null | head -5

Repository: ZingerLittleBee/ServerBee

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Look at the actual checkbox implementation
find apps/web/src/components/ui -name "*checkbox*" -type f

Repository: ZingerLittleBee/ServerBee

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Inspect the production checkbox component
cat -n apps/web/src/components/ui/checkbox.tsx

Repository: ZingerLittleBee/ServerBee

Length of output: 1577


Fix the checkbox mock to align with actual input state transitions.

On Line 85, using !checked to toggle can diverge from the real HTML input's state. Use event.currentTarget.checked to ensure the mock's behavior matches the actual Checkbox component semantics.

Proposed fix
     <input
       checked={checked}
       data-testid="checkbox"
-      onChange={() => onCheckedChange?.(!checked)}
+      onChange={(event) => onCheckedChange?.(event.currentTarget.checked)}
       type="checkbox"
       {...props}
     />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Checkbox: ({
checked,
onCheckedChange,
...props
}: {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
} & Record<string, unknown>) => (
<input
checked={checked}
data-testid="checkbox"
onChange={() => onCheckedChange?.(!checked)}
type="checkbox"
{...props}
/>
)
Checkbox: ({
checked,
onCheckedChange,
...props
}: {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
} & Record<string, unknown>) => (
<input
checked={checked}
data-testid="checkbox"
onChange={(event) => onCheckedChange?.(event.currentTarget.checked)}
type="checkbox"
{...props}
/>
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widget-config-dialog.test.tsx` around lines
74 - 89, The Checkbox mock's onChange currently toggles using !checked which can
drift from real DOM behavior; update the mock component (Checkbox in the test
file) to read the new checked state from the event (use
event.currentTarget.checked) inside the onChange handler and pass that value to
onCheckedChange so the test simulates actual input state transitions
consistently.

Comment thread apps/web/src/components/server/upgrade-job-badge.tsx Outdated
Comment thread apps/web/src/components/uptime/uptime-timeline.test.tsx
Comment on lines +351 to +374
case 'upgrade_progress': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.stage !== 'string'
) {
break
}
const { server_id, target_version, stage } = raw as {
server_id: string
job_id: string
target_version: string
stage: string
}
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
if (existingJob) {
useUpgradeJobsStore.getState().setJob(server_id, {
...existingJob,
stage: stage as UpgradeJob['stage'],
target_version
})
}
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

TypeScript narrowing issue causes pipeline failure at line 360.

The pipeline shows TS2352 error: TypeScript doesn't recognize that the validation checks (lines 352-357) narrow the type. The cast at line 360 fails because TypeScript sees raw as { type: string } & Record<string, unknown> which doesn't satisfy the target type.

🐛 Proposed fix to satisfy TypeScript
     case 'upgrade_progress': {
       if (
         typeof raw.server_id !== 'string' ||
         typeof raw.job_id !== 'string' ||
         typeof raw.target_version !== 'string' ||
         typeof raw.stage !== 'string'
       ) {
         break
       }
-      const { server_id, target_version, stage } = raw as {
-        server_id: string
-        job_id: string
-        target_version: string
-        stage: string
-      }
+      const server_id = raw.server_id as string
+      const target_version = raw.target_version as string
+      const stage = raw.stage as string
       const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
       if (existingJob) {
         useUpgradeJobsStore.getState().setJob(server_id, {
           ...existingJob,
           stage: stage as UpgradeJob['stage'],
           target_version
         })
       }
       break
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'upgrade_progress': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.stage !== 'string'
) {
break
}
const { server_id, target_version, stage } = raw as {
server_id: string
job_id: string
target_version: string
stage: string
}
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
if (existingJob) {
useUpgradeJobsStore.getState().setJob(server_id, {
...existingJob,
stage: stage as UpgradeJob['stage'],
target_version
})
}
break
case 'upgrade_progress': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.stage !== 'string'
) {
break
}
const server_id = raw.server_id as string
const target_version = raw.target_version as string
const stage = raw.stage as string
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
if (existingJob) {
useUpgradeJobsStore.getState().setJob(server_id, {
...existingJob,
stage: stage as UpgradeJob['stage'],
target_version
})
}
break
🧰 Tools
🪛 GitHub Actions: CI

[error] 360-360: TS2352: Conversion may be a mistake when casting '{ type: string; } & Record<string, unknown>' to '{ server_id: string; job_id: string; target_version: string; stage: string; }'. Missing properties: server_id, job_id, target_version, stage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-servers-ws.ts` around lines 351 - 374, TypeScript
isn't narrowing `raw` in the 'upgrade_progress' branch, so replace the ad-hoc
typeof checks + cast with a user-defined type guard; add a function like
isUpgradeProgressPayload(x): x is { server_id: string; job_id: string;
target_version: string; stage: string } and call if
(!isUpgradeProgressPayload(raw)) break; then safely destructure const {
server_id, job_id, target_version, stage } = raw and continue to call
useUpgradeJobsStore.getState().getJob(server_id) and setJob(...) using
UpgradeJob['stage'] as needed.

Comment on lines +376 to +407
case 'upgrade_result': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.status !== 'string'
) {
break
}
const { server_id, job_id, target_version, status, stage, error, backup_path } = raw as {
server_id: string
job_id: string
target_version: string
status: string
stage?: string
error?: string | null
backup_path?: string | null
}
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
const now = new Date().toISOString()
useUpgradeJobsStore.getState().setJob(server_id, {
server_id,
job_id,
target_version,
stage: (stage as UpgradeJob['stage']) ?? existingJob?.stage ?? 'downloading',
status: status as UpgradeJob['status'],
error: error ?? null,
backup_path: backup_path ?? null,
started_at: existingJob?.started_at ?? now,
finished_at: now
})
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

TypeScript narrowing issue causes pipeline failure at line 385.

Same issue as upgrade_progress - the cast fails TypeScript validation despite the runtime checks.

🐛 Proposed fix to satisfy TypeScript
     case 'upgrade_result': {
       if (
         typeof raw.server_id !== 'string' ||
         typeof raw.job_id !== 'string' ||
         typeof raw.target_version !== 'string' ||
         typeof raw.status !== 'string'
       ) {
         break
       }
-      const { server_id, job_id, target_version, status, stage, error, backup_path } = raw as {
-        server_id: string
-        job_id: string
-        target_version: string
-        status: string
-        stage?: string
-        error?: string | null
-        backup_path?: string | null
-      }
+      const server_id = raw.server_id as string
+      const job_id = raw.job_id as string
+      const target_version = raw.target_version as string
+      const status = raw.status as string
+      const stage = raw.stage as string | undefined
+      const error = raw.error as string | null | undefined
+      const backup_path = raw.backup_path as string | null | undefined
       const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
       const now = new Date().toISOString()
       useUpgradeJobsStore.getState().setJob(server_id, {
         server_id,
         job_id,
         target_version,
         stage: (stage as UpgradeJob['stage']) ?? existingJob?.stage ?? 'downloading',
         status: status as UpgradeJob['status'],
         error: error ?? null,
         backup_path: backup_path ?? null,
         started_at: existingJob?.started_at ?? now,
         finished_at: now
       })
       break
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'upgrade_result': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.status !== 'string'
) {
break
}
const { server_id, job_id, target_version, status, stage, error, backup_path } = raw as {
server_id: string
job_id: string
target_version: string
status: string
stage?: string
error?: string | null
backup_path?: string | null
}
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
const now = new Date().toISOString()
useUpgradeJobsStore.getState().setJob(server_id, {
server_id,
job_id,
target_version,
stage: (stage as UpgradeJob['stage']) ?? existingJob?.stage ?? 'downloading',
status: status as UpgradeJob['status'],
error: error ?? null,
backup_path: backup_path ?? null,
started_at: existingJob?.started_at ?? now,
finished_at: now
})
break
case 'upgrade_result': {
if (
typeof raw.server_id !== 'string' ||
typeof raw.job_id !== 'string' ||
typeof raw.target_version !== 'string' ||
typeof raw.status !== 'string'
) {
break
}
const server_id = raw.server_id as string
const job_id = raw.job_id as string
const target_version = raw.target_version as string
const status = raw.status as string
const stage = raw.stage as string | undefined
const error = raw.error as string | null | undefined
const backup_path = raw.backup_path as string | null | undefined
const existingJob = useUpgradeJobsStore.getState().getJob(server_id)
const now = new Date().toISOString()
useUpgradeJobsStore.getState().setJob(server_id, {
server_id,
job_id,
target_version,
stage: (stage as UpgradeJob['stage']) ?? existingJob?.stage ?? 'downloading',
status: status as UpgradeJob['status'],
error: error ?? null,
backup_path: backup_path ?? null,
started_at: existingJob?.started_at ?? now,
finished_at: now
})
break
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 385-385: TS2352: Conversion may be a mistake when casting '{ type: string; } & Record<string, unknown>' to '{ server_id: string; job_id: string; target_version: string; status: string; stage?: string | undefined; error?: string | null | undefined; backup_path?: string | null | undefined; }'. Missing properties: server_id, job_id, target_version, status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-servers-ws.ts` around lines 376 - 407, The TypeScript
error comes from casting raw to a complex type inside the 'upgrade_result' case;
after your runtime typeof checks, stop using the inline "as { ... }" cast and
instead build a properly typed payload object from the validated raw fields
(e.g. create const payload = { server_id: raw.server_id, job_id: raw.job_id,
target_version: raw.target_version, status: raw.status, stage: raw.stage, error:
raw.error ?? null, backup_path: raw.backup_path ?? null } and then narrow
stage/status into UpgradeJob['stage'] and UpgradeJob['status'] as needed), then
call useUpgradeJobsStore.getState().setJob(server_id, { ...payload, stage:
payload.stage ?? existingJob?.stage ?? 'downloading', started_at:
existingJob?.started_at ?? now, finished_at: now }); this avoids the failing
cast while preserving your runtime checks and uses the existing functions getJob
and setJob and the UpgradeJob types.

Comment on lines +1952 to +1963
if let Err(error) = cmd.spawn() {
emit_upgrade_failure(
&tx,
None,
version.to_string(),
UpgradeStage::Restarting,
error.to_string(),
Some(backup_path.display().to_string()),
)
.await;
return Err(error.into());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

job_id is None on spawn failure instead of the actual job ID.

When the new process fails to spawn, the failure is emitted with job_id: None, which prevents the server from correlating this failure with the tracked job. The job_id should be passed through.

Proposed fix
-    if let Err(error) = cmd.spawn() {
+    let job_id_for_failure = job_id.clone();
+    if let Err(error) = cmd.spawn() {
         emit_upgrade_failure(
             &tx,
-            None,
+            job_id_for_failure,
             version.to_string(),
             UpgradeStage::Restarting,
             error.to_string(),
             Some(backup_path.display().to_string()),
         )
         .await;
         return Err(error.into());
     }

Note: Since job_id is consumed by emit_upgrade_progress on line 1945, you'll need to clone it earlier or restructure the ownership.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/agent/src/reporter.rs` around lines 1952 - 1963, The failure emission
currently passes job_id: None when cmd.spawn() fails because job_id was
previously moved; locate the code around emit_upgrade_progress and the
cmd.spawn() block and preserve the job id by cloning it before it is consumed
(or change ownership to borrow) so you can pass Some(job_id.clone()) into
emit_upgrade_failure(&tx, Some(job_id.clone()), version.to_string(),
UpgradeStage::Restarting, error.to_string(),
Some(backup_path.display().to_string())). Ensure you clone the original job_id
variable right before the call that consumes it (the emit_upgrade_progress call
at/near where job_id is currently moved) so the spawn failure path has the real
job id to report.

Comment on lines +571 to +589
let asset = state
.upgrade_release_service
.resolve_asset(version, &asset_name)
.await?;

let sender = state
.agent_manager
.get_sender(&id)
.ok_or_else(|| AppError::NotFound("Agent not connected".into()))?;

let job = state
.upgrade_tracker
.start_job(&id, version.to_string())
.map_err(|error| match error {
StartUpgradeJobError::Conflict(existing) => AppError::Conflict(format!(
"Upgrade already running for server {} (job_id={}, target_version={})",
existing.server_id, existing.job_id, existing.target_version
)),
})?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast on no-op or locally-denied upgrades before job creation.

This block can still fetch release metadata and open a job even when the connected agent is already on the requested version or when its effective runtime capabilities do not include CAP_UPGRADE. The first case can self-resolve as "succeeded" on the next SystemInfo; the second just creates a doomed job that the agent will bounce with CapabilityDenied.

🧭 Suggested preflight checks
+    if server
+        .agent_version
+        .as_deref()
+        .is_some_and(|current| normalize_version(current) == version)
+    {
+        return Err(AppError::Conflict(
+            "Agent is already running the requested version".into(),
+        ));
+    }
+
+    if let Some(effective_caps) = state.agent_manager.get_effective_capabilities(&id)
+        && !has_capability(effective_caps, CAP_UPGRADE)
+    {
+        return Err(AppError::Forbidden(
+            "Upgrade capability is disabled by the connected agent".into(),
+        ));
+    }
+
     let asset = state
         .upgrade_release_service
         .resolve_asset(version, &asset_name)

As per coding guidelines Validate capabilities on both server and agent sides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/server/src/router/api/server.rs` around lines 571 - 589, Before
creating a job, perform preflight checks: after getting the sender via
state.agent_manager.get_sender(&id) but before calling
state.upgrade_release_service.resolve_asset(...) and
state.upgrade_tracker.start_job(...), read the agent's current runtime/system
info (e.g., from the returned sender or agent record) to (1) detect a no-op
upgrade (agent already at the requested version) and return an immediate
success/Idempotent response, and (2) verify the agent's effective capabilities
include CAP_UPGRADE and return a suitable error (e.g., AppError::Forbidden or
AppError::Conflict) if missing; implement these checks using the existing
symbols (state.agent_manager.get_sender, CAP_UPGRADE,
state.upgrade_release_service.resolve_asset, state.upgrade_tracker.start_job,
StartUpgradeJobError::Conflict) so the code fails fast before fetching assets or
creating jobs.

Comment on lines +84 to +88
if let Some(cached) = cache.as_ref()
&& !cached.is_expired()
{
return cached.response();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Rust version requirements in the project
cat Cargo.toml | grep -i "rust-version\|edition"
fd "Cargo.toml" crates --exec grep -l "rust-version" {} \; 2>/dev/null | head -5 | xargs cat 2>/dev/null | grep -i "rust-version"
rustc --version 2>/dev/null || echo "rustc not found in sandbox"

Repository: ZingerLittleBee/ServerBee

Length of output: 112


🌐 Web query:

Rust let-chains stabilized version when released

💡 Result:

Rust let-chains (the let_chains feature, allowing &&-chaining of let statements in if and while conditions) was stabilized in Rust 1.88.0, released on June 26, 2025. It is available without a feature flag only in the Rust 2024 edition and later. The stabilization PR (#132833) was merged on April 22, 2025, and included in the 1.88.0 release notes. Earlier partial stabilization attempts (e.g., PR #94927 for 1.64) were limited or reverted due to issues with drop order semantics, finalized properly for edition 2024.

Citations:


🌐 Web query:

Rust 2024 edition MSRV minimum supported version requirement

💡 Result:

There is no minimum supported Rust version (MSRV) requirement for using the Rust 2024 edition. The Rust 2024 edition was stabilized and released in Rust 1.85.0 (February 20, 2025). Any Rust version >=1.85.0 fully supports compiling code with edition="2024" in Cargo.toml. The rust-version field in Cargo.toml is optional and declares the MSRV that crate authors choose for their package; it is not tied to the edition. Edition 2024 enables Cargo's rust-version-aware resolver by default (resolver="3"), which respects dependencies' rust-version during resolution, but this does not impose an MSRV on the project itself. Projects can set edition="2024" without specifying rust-version (defaulting to no declared MSRV), and it will compile on Rust 1.85+. Older Rust versions (<1.85) cannot compile edition 2024 code due to lacking edition support, but there is no further restriction.

Citations:


Update MSRV or Rust version requirement to match let-chains stabilization in edition 2024.

Let-chains syntax was stabilized in Rust 1.88.0 (released June 26, 2025) and is available without a feature flag only in the 2024 edition. The project currently uses edition = "2024" without an explicit rust-version field in Cargo.toml. To ensure the code compiles, the project must require Rust 1.88.0 or later. Set rust-version = "1.88" in Cargo.toml or document the MSRV requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/server/src/service/upgrade_release.rs` around lines 84 - 88, The code
in upgrade_release.rs uses let-chain syntax (the if let Some(cached) =
cache.as_ref() && !cached.is_expired() { return cached.response(); }) which
requires Rust 1.88+/edition 2024; update the project MSRV by adding rust-version
= "1.88" to Cargo.toml (or document the requirement), or alternatively rewrite
the branch to avoid let-chains (e.g., match or nested ifs) if you cannot raise
the MSRV; reference the let-chain line around cache.as_ref(),
cached.is_expired(), and cached.response() when making the change.

Comment thread tests/agent-upgrade.md
Comment on lines +91 to +101
### 6. Verify Concurrent Upgrade Rejection

**Steps:**
1. Start an upgrade on server A
2. While upgrade is in progress, attempt to start upgrade on server B
3. Or rapidly click upgrade button multiple times on same server

**Expected:**
- Second upgrade attempt rejected with "Upgrade already in progress" message
- UI prevents concurrent upgrade initiation
- First upgrade continues unaffected
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make this scenario same-server.

UpgradeJobTracker conflicts on server_id, so an upgrade on server B should be allowed. The rejection case here is only "start another upgrade on the same server while one is already running."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/agent-upgrade.md` around lines 91 - 101, The test scenario should be
changed to verify same-server concurrent upgrade rejection because
UpgradeJobTracker conflicts on server_id; update the steps and expectations in
the "Verify Concurrent Upgrade Rejection" section to start an upgrade on a
single server (e.g., Server A), then attempt to start another upgrade on that
same server (or rapidly click the upgrade button on that same server) and assert
UpgradeJobTracker rejects the second attempt with the "Upgrade already in
progress" message and the UI prevents initiating a second upgrade while the
first continues; ensure all mentions of "server B" or different-server attempts
are removed and reference UpgradeJobTracker and server_id as the concurrency key
in the scenario description.

Comment thread tests/agent-upgrade.md
Comment on lines +124 to +173
### 8. Test WebSocket Real-Time Updates

**Steps:**
1. Open browser DevTools Network tab
2. Connect to WebSocket `/api/ws/servers`
3. Trigger upgrade from another browser/session
4. Monitor WebSocket messages

**Expected:**
- `CapabilitiesChanged` message received when upgrade starts (capability temporarily disabled)
- `ServerUpdate` messages with upgrade progress in payload
- `CapabilitiesChanged` message received when upgrade completes (capability re-enabled)
- UI updates in real-time without page refresh

---

### 9. Verify Capability Check

**Steps:**
1. Disable `CAP_UPGRADE` on a server (via database or API)
2. Navigate to that server's detail page

**Expected:**
- "Upgrade Agent" button is hidden
- Upgrade option not available in UI
- Attempting upgrade via API returns capability error

---

### 10. Rollback Verification

**Steps:**
1. Trigger upgrade
2. During `installing` stage, force agent disconnect (kill process)
3. Restart agent manually

**Expected:**
- Agent starts with previous version (backup restored)
- Server detects version mismatch on reconnect
- Upgrade can be retried

---

## API Endpoints

| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/api/servers/:id/upgrade` | POST | Admin | Trigger agent upgrade |
| `/api/servers/:id/upgrade-status` | GET | Admin | Get current upgrade status |
| `/api/ws/servers` | WS | Session | Real-time upgrade progress |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the checklist to the actual WS/API surface.

The upgrade flow in this PR is driven by full_sync.upgrades, upgrade_progress, and upgrade_result, and the provided router only exposes POST /api/servers/{id}/upgrade. Looking for CapabilitiesChanged, ServerUpdate progress payloads, or GET /api/servers/:id/upgrade-status will send QA after behavior that is not implemented here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/agent-upgrade.md` around lines 124 - 173, The checklist references WS
messages and a GET endpoint that don't exist; update the test steps and API
surface to match the actual upgrade flow driven by full_sync.upgrades,
upgrade_progress, and upgrade_result and the router that only exposes POST
/api/servers/{id}/upgrade; remove or replace references to
CapabilitiesChanged/ServerUpdate WebSocket messages and
/api/servers/:id/upgrade-status, and instead instruct QA to observe the
full_sync.upgrades event stream and the upgrade_progress/upgrade_result payloads
(or the single POST call response and subsequent state changes) when validating
behavior.

@ZingerLittleBee ZingerLittleBee merged commit 4e1b367 into main Apr 14, 2026
10 checks passed
@ZingerLittleBee ZingerLittleBee deleted the feat/agent-self-upgrade-closure branch April 14, 2026 14:29
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.

1 participant