fix: keep one policy object per change so the login page renders in the desktop app

This commit is contained in:
Linus Rath
2026-11-03 10:45:45 +01:00
parent a9b6a05841
commit fcf31df928
4 changed files with 37 additions and 5 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ function serve(plugins: unknown[]) {
beforeEach(() => {
vi.clearAllMocks();
usePluginStore.setState({ plugins: [], initialized: false });
harness.current = snapshotOf({});
setPolicy({});
localStorage.clear();
usePluginStore.setState({ plugins: [], initialized: false });
});
+3 -3
View File
@@ -106,8 +106,8 @@ describe('loadSandboxedPlugin under the OS policy of the device', () => {
it('starts nothing the policy refuses: no frame, no download', async () => {
const { EMPTY_POLICY_LAYERS, resolvePolicies } = await import('../desktop/policy-schema');
const snapshot = (forcedMachine: Record<string, unknown>) => resolvePolicies({ ...EMPTY_POLICY_LAYERS, forcedMachine });
let current = snapshot({ PluginsMode: 'allowlist', PluginAllowlist: ['other-plugin'] });
window.bulwarkDesktop = { version: 1, platform: 'win32', policy: { get: () => current, onChange: () => () => {}, onWipeOfflineData: () => () => {} } };
const bridgeWith = (current: ReturnType<typeof snapshot>) => ({ version: 1, platform: 'win32' as const, policy: { get: () => current, onChange: () => () => {}, onWipeOfflineData: () => () => {} } });
window.bulwarkDesktop = bridgeWith(snapshot({ PluginsMode: 'allowlist', PluginAllowlist: ['other-plugin'] }));
mocks.getCode.mockResolvedValue(CODE);
try {
await loadSandboxedPlugin(plugin());
@@ -116,7 +116,7 @@ describe('loadSandboxedPlugin under the OS policy of the device', () => {
expect(mocks.downloadManagedBundle).not.toHaveBeenCalled();
expect(mocks.getCode).not.toHaveBeenCalled();
current = snapshot({ PluginsMode: 'allowlist', PluginAllowlist: ['managed-plugin'] });
window.bulwarkDesktop = bridgeWith(snapshot({ PluginsMode: 'allowlist', PluginAllowlist: ['managed-plugin'] }));
await loadSandboxedPlugin(plugin());
expect(lastStatus()).toMatchObject({ id: 'managed-plugin', status: 'running' });
} finally {
@@ -85,6 +85,19 @@ describe('in the app', () => {
expect(devicesForAccount('some-account').noOffline).toBe(false);
}, 60_000);
it('the hook settles on a bridge that copies what it returns, as contextBridge does', async () => {
setBridgePolicy(snapshotOf({ PluginsMode: 'off' }));
window.bulwarkDesktop!.policy!.get = () => JSON.parse(JSON.stringify(current));
let renders = 0;
function Probe() {
renders += 1;
return <span>{useDesktopPolicy().PluginsMode.value}</span>;
}
render(<Probe />);
expect(screen.getByText('off')).toBeInTheDocument();
expect(renders).toBeLessThan(4);
});
it('the hook shows a change at once', async () => {
setBridgePolicy(snapshotOf({}));
function Probe() {
+20 -1
View File
@@ -8,6 +8,7 @@
import { useSyncExternalStore } from 'react';
import { getDesktopBridge } from './bridge';
import type { DesktopPolicyBridge } from './types';
import {
DEFAULT_POLICY_SNAPSHOT,
effectivePolicyValue,
@@ -21,11 +22,29 @@ import {
/** A plugin the device's policy refuses, on a plugin's record and in the settings page. */
export const PLUGIN_POLICY_REFUSED_ERROR = 'device-policy';
let cached: { bridge: DesktopPolicyBridge; snapshot: PolicySnapshot } | null = null;
/**
* The same object until the policy changes. The bridge can't give that: contextBridge
* copies what it returns, so each `get()` is a new object, and useSyncExternalStore takes
* a new object for a change and renders forever.
*/
export function getDesktopPolicy(): PolicySnapshot {
return getDesktopBridge()?.policy?.get?.() ?? DEFAULT_POLICY_SNAPSHOT;
const bridge = getDesktopBridge()?.policy;
if (!bridge?.get) return DEFAULT_POLICY_SNAPSHOT;
if (cached?.bridge !== bridge) {
const entry = { bridge, snapshot: bridge.get() };
cached = entry;
bridge.onChange?.((snapshot) => {
entry.snapshot = snapshot;
});
}
return cached.snapshot;
}
export function onDesktopPolicyChange(listener: (snapshot: PolicySnapshot) => void): () => void {
// First, so what the listener reads is already the new value.
getDesktopPolicy();
return getDesktopBridge()?.policy?.onChange?.(listener) ?? (() => {});
}