Skip to content

fix: prevent silent upgrade failures from locking users on old versions#428

Closed
anandgupta42 wants to merge 1 commit intomainfrom
fix/upgrade-robustness
Closed

fix: prevent silent upgrade failures from locking users on old versions#428
anandgupta42 wants to merge 1 commit intomainfrom
fix/upgrade-robustness

Conversation

@anandgupta42
Copy link
Copy Markdown
Contributor

@anandgupta42 anandgupta42 commented Mar 24, 2026

The upgrade path is the most critical code — if it breaks, users are permanently locked out. Two fixes:

  1. Replace .catch(() => {}) in worker.ts with error logging so upgrade failures are visible instead of silently swallowed
  2. Add comprehensive tests (41 total) including:
    • semver bundling smoke tests (verify import works, not externalized)
    • Decision logic tests for every branch in upgrade()
    • Source assertion that checkUpgrade logs errors (not empty catch)
    • Installation.VERSION format validation

Keeps semver — it bundles correctly into the compiled binary (verified: not in build.ts external list). The real issue was the silent .catch.

Summary

What changed and why?

Test Plan

How was this tested?

Checklist

  • Tests added/updated
  • Documentation updated (if needed)
  • CHANGELOG updated (if user-facing)

Summary by CodeRabbit

  • Bug Fixes

    • Upgrade check failures are now logged to console instead of being silently ignored, improving visibility into upgrade check status.
  • Tests

    • Added comprehensive test coverage for upgrade decision logic, version edge case handling, and semver validation.

The upgrade path is the most critical code — if it breaks, users are
permanently locked out. Two fixes:

1. Replace `.catch(() => {})` in worker.ts with error logging so upgrade
   failures are visible instead of silently swallowed
2. Add comprehensive tests (41 total) including:
   - semver bundling smoke tests (verify import works, not externalized)
   - Decision logic tests for every branch in upgrade()
   - Source assertion that checkUpgrade logs errors (not empty catch)
   - Installation.VERSION format validation

Keeps semver — it bundles correctly into the compiled binary (verified:
not in build.ts external list). The real issue was the silent .catch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

The PR modifies error handling in the upgrade check utility to log failures via console error instead of silently ignoring them, and significantly expands test coverage to validate semver dependency availability and upgrade error handling behavior.

Changes

Cohort / File(s) Summary
Upgrade Error Handling
packages/opencode/src/cli/cmd/tui/worker.ts
Changed upgrade() error catching to log failures (console.error("[upgrade] check failed:", ...)) instead of silently swallowing them with empty catch block.
Test Suite Expansion
packages/opencode/test/cli/upgrade-decision.test.ts
Restructured tests, removed "equal versions edge case" check, added new version edge case tests (patch/major bumps, prerelease ordering), introduced semver bundling health suite validating semver API availability, and added upgrade() module health suite performing static source inspection of worker.ts to confirm error handling is observable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested labels

contributor

Poem

🐰 A rabbit hops through logs so bright,
Where errors now shine into light,
No more secrets swept away,
Tests keep watchers on the way,
Semver bundled, upgrade tracked right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides context and test details but duplicates the template sections without clearly filling them out; the template requires distinct sections for Summary, Test Plan, and Checklist. Restructure the description to clearly separate the template sections: provide a concise Summary, explain the Test Plan methodology, and explicitly check/uncheck the Checklist items.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: preventing silent upgrade failures by adding error logging instead of swallowing errors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 fix/upgrade-robustness
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/upgrade-robustness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 1

🧹 Nitpick comments (2)
packages/opencode/src/cli/cmd/tui/worker.ts (1)

289-293: Consider using Log.Default.error for consistency.

The rest of this file uses Log.Default.error() for error logging (lines 28–36, 251–253), but this change uses console.error() directly. Unless there's a specific reason (e.g., Log not being initialized at this point), consider aligning with the existing pattern for consistent observability.

♻️ Suggested change
         await upgrade().catch((err) => {
           // Never silently swallow upgrade errors — if this fails, users
           // get locked on old versions with no way to self-heal.
-          console.error("[upgrade] check failed:", String(err))
+          Log.Default.error("[upgrade] check failed", {
+            error: err instanceof Error ? err.message : String(err),
+          })
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/worker.ts` around lines 289 - 293, The
upgrade error handler is using console.error instead of the project's logging
abstraction; update the catch for the await upgrade() call to call
Log.Default.error(...) with the same message and error details (e.g.,
Log.Default.error("[upgrade] check failed: %s", String(err))) so logging is
consistent with other uses in this file (see other Log.Default.error usages and
the upgrade() call). Ensure Log is in scope/imported where upgrade() is invoked
before replacing console.error.
packages/opencode/test/cli/upgrade-decision.test.ts (1)

192-193: Consider using ES imports or Bun APIs for consistency.

The test file uses ES module imports at the top but switches to require("fs") and require("path") inline. For consistency, consider using Bun.file() or top-level imports.

♻️ Alternative using Bun.file()
   test("semver is NOT in the build external list", async () => {
-    const fs = require("fs")
-    const path = require("path")
-    const buildScript = fs.readFileSync(
-      path.join(import.meta.dir, "../../script/build.ts"),
-      "utf-8",
-    )
+    const buildScript = await Bun.file(
+      import.meta.dir + "/../../script/build.ts"
+    ).text()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/upgrade-decision.test.ts` around lines 192 - 193,
Replace the inline CommonJS calls const fs = require("fs") and const path =
require("path") with consistent ES-style imports or Bun APIs used elsewhere in
the file; e.g., add top-level imports (import fs from "fs"; import path from
"path") or use Bun.file()/Bun.stat() where file I/O is needed so the test
matches the module style and runtime expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/test/cli/upgrade-decision.test.ts`:
- Around line 189-204: The test "semver is NOT in the build external list" can
silently succeed when the regex fails; update it to explicitly fail when no
external array is found by asserting that externalMatch is truthy (or throwing a
clear error) before inspecting externalMatch[1]; locate the variables
buildScript and externalMatch in that test and add an explicit expectation like
expect(externalMatch).toBeTruthy() with a message such as "external array not
found in build.ts" so the test fails loudly if build.ts format changes.

---

Nitpick comments:
In `@packages/opencode/src/cli/cmd/tui/worker.ts`:
- Around line 289-293: The upgrade error handler is using console.error instead
of the project's logging abstraction; update the catch for the await upgrade()
call to call Log.Default.error(...) with the same message and error details
(e.g., Log.Default.error("[upgrade] check failed: %s", String(err))) so logging
is consistent with other uses in this file (see other Log.Default.error usages
and the upgrade() call). Ensure Log is in scope/imported where upgrade() is
invoked before replacing console.error.

In `@packages/opencode/test/cli/upgrade-decision.test.ts`:
- Around line 192-193: Replace the inline CommonJS calls const fs =
require("fs") and const path = require("path") with consistent ES-style imports
or Bun APIs used elsewhere in the file; e.g., add top-level imports (import fs
from "fs"; import path from "path") or use Bun.file()/Bun.stat() where file I/O
is needed so the test matches the module style and runtime expectations.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 12ae05e0-ce08-4b27-8a92-c20e67fde996

📥 Commits

Reviewing files that changed from the base of the PR and between 966f43c and 756dec9.

📒 Files selected for processing (2)
  • packages/opencode/src/cli/cmd/tui/worker.ts
  • packages/opencode/test/cli/upgrade-decision.test.ts

Comment on lines +189 to +204
test("semver is NOT in the build external list", () => {
// If semver is externalized, it won't be bundled in the compiled binary.
// This test reads build.ts to verify it's not listed.
const fs = require("fs")
const path = require("path")
const buildScript = fs.readFileSync(
path.join(import.meta.dir, "../../script/build.ts"),
"utf-8",
)
// Extract the external array
const externalMatch = buildScript.match(/external:\s*\[([\s\S]*?)\]/)
if (externalMatch) {
expect(externalMatch[1]).not.toContain('"semver"')
expect(externalMatch[1]).not.toContain("'semver'")
}
})
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

Test silently passes if build script format changes.

If the regex doesn't find an external: array (e.g., because build.ts is refactored), externalMatch is null and no assertions run—giving a false sense of security. Consider making the expectation explicit.

🛡️ Suggested fix
     const externalMatch = buildScript.match(/external:\s*\[([\s\S]*?)\]/)
-    if (externalMatch) {
-      expect(externalMatch[1]).not.toContain('"semver"')
-      expect(externalMatch[1]).not.toContain("'semver'")
-    }
+    // If there's an external array, ensure semver is not in it.
+    // If no external array exists, bundler bundles everything by default.
+    if (externalMatch) {
+      expect(externalMatch[1]).not.toContain('"semver"')
+      expect(externalMatch[1]).not.toContain("'semver'")
+    } else {
+      // Explicitly acknowledge no external array was found (semver bundled by default)
+      expect(buildScript).toContain("Bun.build") // sanity check we're reading the right file
+    }
📝 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
test("semver is NOT in the build external list", () => {
// If semver is externalized, it won't be bundled in the compiled binary.
// This test reads build.ts to verify it's not listed.
const fs = require("fs")
const path = require("path")
const buildScript = fs.readFileSync(
path.join(import.meta.dir, "../../script/build.ts"),
"utf-8",
)
// Extract the external array
const externalMatch = buildScript.match(/external:\s*\[([\s\S]*?)\]/)
if (externalMatch) {
expect(externalMatch[1]).not.toContain('"semver"')
expect(externalMatch[1]).not.toContain("'semver'")
}
})
test("semver is NOT in the build external list", () => {
// If semver is externalized, it won't be bundled in the compiled binary.
// This test reads build.ts to verify it's not listed.
const fs = require("fs")
const path = require("path")
const buildScript = fs.readFileSync(
path.join(import.meta.dir, "../../script/build.ts"),
"utf-8",
)
// Extract the external array
const externalMatch = buildScript.match(/external:\s*\[([\s\S]*?)\]/)
// If there's an external array, ensure semver is not in it.
// If no external array exists, bundler bundles everything by default.
if (externalMatch) {
expect(externalMatch[1]).not.toContain('"semver"')
expect(externalMatch[1]).not.toContain("'semver'")
} else {
// Explicitly acknowledge no external array was found (semver bundled by default)
expect(buildScript).toContain("Bun.build") // sanity check we're reading the right file
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/upgrade-decision.test.ts` around lines 189 - 204,
The test "semver is NOT in the build external list" can silently succeed when
the regex fails; update it to explicitly fail when no external array is found by
asserting that externalMatch is truthy (or throwing a clear error) before
inspecting externalMatch[1]; locate the variables buildScript and externalMatch
in that test and add an explicit expectation like
expect(externalMatch).toBeTruthy() with a message such as "external array not
found in build.ts" so the test fails loudly if build.ts format changes.

@anandgupta42 anandgupta42 deleted the fix/upgrade-robustness branch March 25, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant