Files
Shadowbroker/frontend/src/__tests__/desktop/updateRuntime.test.ts
T
Shadowbroker 2dc1fcc778 release: v0.9.81 — signed auto-update + admin_session race fix (#323)
What this release does
----------------------

1. Establishes a fresh Tauri updater signing keypair. The previous keypair
   (pubkey baked into v0.9.79 / v0.9.8) had no matching private key on
   any maintainer-controlled machine — every prior release shipped
   without signatures, so auto-update has never actually worked. v0.9.81
   rotates to a new pubkey and ships signed installers + latest.json so
   every release from here is a one-click upgrade.

2. Fixes the ``admin_session_required`` race in TopRightControls.tsx.
   The updateAction state used to default to ``auto_apply`` at React-init
   time. A click on the Update button before the async runtime probe
   completed went down the auto_apply path (POST /api/system/update),
   which throws ``admin_session_required`` on fresh sessions. Desktop
   installs now default to ``manual_download`` based on synchronous
   ``window.__TAURI__`` detection at useState init.

One-time cost for current installs
----------------------------------

Anyone on v0.9.79 or v0.9.8 will see the in-app Update button still
trigger the broken path on their existing install (the fix only takes
effect once they're ON v0.9.81). The MANUAL DOWNLOAD button in the
update dialog opens the GitHub release page, where they grab the .msi
and run it. After that one manual hop, all future updates are seamless.

Release artifacts
-----------------

  ShadowBroker_v0.9.81.zip                  6.06 MB
    42f8a51f9a5690d1e7349d90d8ecf2d163c9061d6cf90c69ee03647a785437ff
  ShadowBroker_0.9.81_x64_en-US.msi       122.4 MB
    a45b177c26c95d2b28d71592d7147e88ff4e104865f214fde11249d311ec9e25
  ShadowBroker_0.9.81_x64-setup.exe        76.5 MB
    eca884b9d37eeccd0f11c91dcc6f6ae1b3609d9dee72bd73c37c9a427babfef2

Plus .sig files for the .msi and .exe, plus a signed latest.json for
the Tauri updater endpoint.

Sizes match the v0.9.79 / v0.9.8 reference shape within drift for
the new TopRightControls patch.

release_digests.json keeps v0.9.79 + v0.9.8 blocks alongside v0.9.81
so operators still on those versions continue to validate cleanly
during the rollout transition.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:43:53 -06:00

107 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
classifyUpdateRuntime,
getDesktopUpdateContext,
getPreferredManualUpdateUrl,
getUpdateAction,
pickDesktopInstallerUrl,
type GitHubLatestRelease,
} from '@/lib/updateRuntime';
const RELEASE: GitHubLatestRelease = {
html_url: 'https://github.com/BigBodyCobain/Shadowbroker/releases/tag/v0.9.81',
assets: [
{ name: 'ShadowBroker_0.9.81_x64_en-US.msi', browser_download_url: 'https://example.test/windows.msi' },
{ name: 'ShadowBroker_0.9.81_x64-setup.exe', browser_download_url: 'https://example.test/windows-setup.exe' },
{ name: 'ShadowBroker_0.9.81_aarch64.dmg', browser_download_url: 'https://example.test/macos.dmg' },
{ name: 'ShadowBroker_0.9.81_amd64.AppImage', browser_download_url: 'https://example.test/linux.AppImage' },
],
};
describe('updateRuntime', () => {
afterEach(() => {
delete (window as Record<string, unknown>).__TAURI__;
});
describe('getDesktopUpdateContext', () => {
it('returns null when Tauri is not present', async () => {
expect(await getDesktopUpdateContext()).toBeNull();
});
it('invokes desktop_update_context when Tauri is present', async () => {
const invoke = vi.fn().mockResolvedValue({
mode: 'packaged',
platform: 'windows',
is_packaged_build: true,
backend_mode: 'managed',
owns_local_backend: true,
});
(window as Record<string, unknown>).__TAURI__ = { core: { invoke } };
const result = await getDesktopUpdateContext();
expect(invoke).toHaveBeenCalledWith('desktop_update_context');
expect(result).toEqual({
mode: 'packaged',
platform: 'windows',
is_packaged_build: true,
backend_mode: 'managed',
owns_local_backend: true,
});
});
});
describe('runtime classification', () => {
it('classifies browser mode when no desktop context exists', () => {
expect(classifyUpdateRuntime(null)).toBe('browser');
expect(getUpdateAction('browser')).toBe('auto_apply');
});
it('classifies desktop dev mode as auto-apply', () => {
expect(
classifyUpdateRuntime({
mode: 'dev',
platform: 'windows',
is_packaged_build: false,
}),
).toBe('desktop_dev');
expect(getUpdateAction('desktop_dev')).toBe('auto_apply');
});
it('classifies packaged desktop mode as manual-download', () => {
expect(
classifyUpdateRuntime({
mode: 'packaged',
platform: 'windows',
is_packaged_build: true,
}),
).toBe('desktop_packaged');
expect(getUpdateAction('desktop_packaged')).toBe('manual_download');
});
});
describe('installer asset selection', () => {
it('prefers msi installers on windows', () => {
expect(pickDesktopInstallerUrl(RELEASE, 'windows')).toBe('https://example.test/windows.msi');
});
it('prefers dmg installers on macos', () => {
expect(pickDesktopInstallerUrl(RELEASE, 'macos')).toBe('https://example.test/macos.dmg');
});
it('prefers appimage installers on linux', () => {
expect(pickDesktopInstallerUrl(RELEASE, 'linux')).toBe('https://example.test/linux.AppImage');
});
it('falls back to the release page when no platform asset matches', () => {
expect(getPreferredManualUpdateUrl(RELEASE, 'desktop_packaged', 'unknown')).toBe(
RELEASE.html_url,
);
});
it('uses release page for non-packaged runtimes', () => {
expect(getPreferredManualUpdateUrl(RELEASE, 'browser', 'windows')).toBe(RELEASE.html_url);
});
});
});