Files
kandev/apps/backend/internal/plugins/autoupdate_poller_test.go
T
João Salavisa 5db3e48262 feat: add opt-in plugin auto-updater (#1903)
* feat: add opt-in plugin auto-updater

Automatically update installed plugins in the background, off by default
and opt-in. Builds on the existing verified install pipeline — an
auto-update is just an automatic reinstall of a newer catalog package_url.

Two-tier toggle on Settings > Plugins: an instance-wide default (SQLite
plugin_settings) plus a per-plugin override (tri-state auto_update on the
plugin record, carried forward on upgrade). The AutoUpdatePoller
(~90-min cadence + a boot sweep) upgrades only plugins that are currently
active, opted in, and report update_available; disabled/errored plugins
stay on their installed version.

New endpoints: GET/PUT /api/plugins/settings and
PUT /api/plugins/:id/auto-update.

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

* fix(plugins): recheck auto-update eligibility at install and fix test mock

Address PR #1903 review feedback:
- Re-check each plugin's current status + effective opt-in immediately before
  InstallFromURL (eligibleForAutoUpdate), closing the TOCTOU window between the
  candidate snapshot / catalog fetch and the install so a plugin disabled or
  opted out mid-sweep is never reinstalled and reactivated.
- Correct the marketplace spec to describe the real failure behavior (shared
  with manual updates) instead of promising a version rollback the install
  pipeline does not perform.
- Fix the frontend CI failure: the plugins page test mocked the whole
  plugins-api module without the new settings/auto-update functions, so
  useAutoUpdateSettings threw at mount.
- Add an aria-label to the per-plugin auto-update Switch and Reset control.
- Add a use-auto-update-settings hook test (load, failure, optimistic setDefault,
  rollback) and an eligibleForAutoUpdate unit test.

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:04:54 +01:00

37 lines
1.2 KiB
Go

package plugins
import (
"context"
"testing"
)
// TestAutoUpdatePollerStartStopIdempotent proves the poller's goroutine
// ownership is clean: Start/Stop are each idempotent and Stop drains the loop
// (goleak's TestMain catches a leaked poller goroutine). The Service has no
// marketplace attached, so the immediate sweep on Start is a no-op — this test
// exercises the lifecycle, not the update logic (covered by RunAutoUpdatePass
// tests, driven directly rather than through the timer).
func TestAutoUpdatePollerStartStopIdempotent(t *testing.T) {
svc, _, _ := newTestService(t)
p := NewAutoUpdatePoller(svc, testLogger(t))
p.Stop() // stop before start is a no-op
p.Start(context.Background())
p.Start(context.Background()) // second start is a no-op
p.Stop()
p.Stop() // second stop is a no-op
}
// TestAutoUpdatePollerStopCancelsViaContext proves cancelling the parent
// context tears the loop down even without an explicit Stop() (Stop still runs
// for a clean drain).
func TestAutoUpdatePollerStopCancelsViaContext(t *testing.T) {
svc, _, _ := newTestService(t)
p := NewAutoUpdatePoller(svc, testLogger(t))
ctx, cancel := context.WithCancel(context.Background())
p.Start(ctx)
cancel()
p.Stop()
}