mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-13 14:10:28 +02:00
release: prepare v0.9.7
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Tests for getBackendEndpoint() — the runtime-resolved API endpoint
|
||||
* displayed in "Connect" modals for external tool configuration.
|
||||
*
|
||||
* Verifies:
|
||||
* - Returns window.location.origin when window is available
|
||||
* - Returns fallback when window is undefined (SSR)
|
||||
* - Does NOT hardcode :8000
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
describe('getBackendEndpoint', () => {
|
||||
const originalWindow = globalThis.window;
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
// Restore window if we deleted it
|
||||
if (!globalThis.window && originalWindow) {
|
||||
globalThis.window = originalWindow;
|
||||
}
|
||||
});
|
||||
|
||||
it('returns window.location.origin in browser context', async () => {
|
||||
// Default test environment (jsdom) has window defined
|
||||
const { getBackendEndpoint } = await import('@/lib/backendEndpoint');
|
||||
const result = getBackendEndpoint();
|
||||
expect(result).toBe(window.location.origin);
|
||||
expect(result).not.toContain(':8000');
|
||||
});
|
||||
|
||||
it('does not hardcode port 8000', async () => {
|
||||
const { getBackendEndpoint } = await import('@/lib/backendEndpoint');
|
||||
const result = getBackendEndpoint();
|
||||
// The result should be derived from window.location, not a hardcoded backend port
|
||||
expect(result).not.toMatch(/:8000$/);
|
||||
});
|
||||
|
||||
it('returns http://localhost:8000 fallback when window is undefined (SSR)', async () => {
|
||||
// Temporarily remove window to simulate SSR
|
||||
// @ts-expect-error — intentionally removing window for SSR simulation
|
||||
delete globalThis.window;
|
||||
const { getBackendEndpoint } = await import('@/lib/backendEndpoint');
|
||||
const result = getBackendEndpoint();
|
||||
expect(result).toBe('http://localhost:8000');
|
||||
// Restore
|
||||
globalThis.window = originalWindow;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Tests for companion section failure visibility in SettingsPanel.
|
||||
*
|
||||
* Verifies the contract from P6E:
|
||||
* - When fetchCompanionStatus rejects, the companion section still renders
|
||||
* (with an unavailable/error state) rather than silently disappearing
|
||||
* - When fetchCompanionStatus succeeds, normal controls are shown
|
||||
*
|
||||
* These are logic-level tests for the state transitions — they do NOT
|
||||
* render SettingsPanel (which has deep dependency chains). They verify
|
||||
* the decision logic: companionLoadFailed drives visibility.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('companion section visibility contract', () => {
|
||||
it('companionAvailable && companionLoadFailed shows the section (failure path)', () => {
|
||||
// Simulates the render guard: {companionAvailable && (companion || companionLoadFailed) && (...)}
|
||||
const companionAvailable = true;
|
||||
const companion = null; // fetch failed, no status loaded
|
||||
const companionLoadFailed = true;
|
||||
|
||||
const shouldRender = companionAvailable && (companion || companionLoadFailed);
|
||||
expect(shouldRender).toBeTruthy();
|
||||
});
|
||||
|
||||
it('companionAvailable && companion shows the section (success path)', () => {
|
||||
const companionAvailable = true;
|
||||
const companion = { enabled: false, url: null, warning: 'Reduced trust.' };
|
||||
const companionLoadFailed = false;
|
||||
|
||||
const shouldRender = companionAvailable && (companion || companionLoadFailed);
|
||||
expect(shouldRender).toBeTruthy();
|
||||
});
|
||||
|
||||
it('section is hidden when not on desktop (companionAvailable=false)', () => {
|
||||
const companionAvailable = false;
|
||||
const companion = null;
|
||||
const companionLoadFailed = true;
|
||||
|
||||
const shouldRender = companionAvailable && (companion || companionLoadFailed);
|
||||
expect(shouldRender).toBeFalsy();
|
||||
});
|
||||
|
||||
it('section is hidden before first load attempt (no status, no failure)', () => {
|
||||
const companionAvailable = true;
|
||||
const companion = null;
|
||||
const companionLoadFailed = false;
|
||||
|
||||
const shouldRender = companionAvailable && (companion || companionLoadFailed);
|
||||
expect(shouldRender).toBeFalsy();
|
||||
});
|
||||
|
||||
it('controls are hidden when companion is null (failure mode shows only error)', () => {
|
||||
// In the rendered UI: {companion && (<buttons>)} — buttons hidden when companion is null
|
||||
const companion = null;
|
||||
const companionLoadFailed = true;
|
||||
|
||||
const showControls = !!companion;
|
||||
expect(showControls).toBe(false);
|
||||
// But the section itself should still render
|
||||
expect(companionLoadFailed).toBe(true);
|
||||
});
|
||||
|
||||
it('warning box only renders when companion has a warning string', () => {
|
||||
// {companion?.warning && (<warning>)}
|
||||
const companionNull = null;
|
||||
const companionWithWarning = { enabled: true, url: 'http://127.0.0.1:9876', warning: 'Reduced trust.' };
|
||||
const companionNoWarning = { enabled: false, url: null, warning: '' };
|
||||
|
||||
expect(companionNull?.warning).toBeFalsy();
|
||||
expect(companionWithWarning?.warning).toBeTruthy();
|
||||
expect(companionNoWarning?.warning).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,387 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ShadowbrokerDesktopRuntime } from '@/lib/desktopBridge';
|
||||
|
||||
describe('desktopBridgeBootstrapPreference', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
// Clean window globals before each test
|
||||
delete (window as Record<string, unknown>).__SHADOWBROKER_DESKTOP__;
|
||||
delete (window as Record<string, unknown>).__SHADOWBROKER_LOCAL_CONTROL__;
|
||||
});
|
||||
|
||||
it('prefers a pre-installed native runtime over the HTTP shim', async () => {
|
||||
const nativeInvoke = vi.fn().mockResolvedValue({ ok: true });
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: nativeInvoke,
|
||||
getNativeControlAuditReport: () => ({
|
||||
totalEvents: 0,
|
||||
totalRecorded: 0,
|
||||
recent: [],
|
||||
byOutcome: {},
|
||||
}),
|
||||
clearNativeControlAuditReport: vi.fn(),
|
||||
};
|
||||
|
||||
// Simulate Tauri injection: set __SHADOWBROKER_DESKTOP__ before bootstrap
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { bootstrapDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
const installed = bootstrapDesktopControlBridge();
|
||||
|
||||
expect(installed).toBe(true);
|
||||
// The bridge should have been derived from the native runtime
|
||||
expect(window.__SHADOWBROKER_LOCAL_CONTROL__).toBeDefined();
|
||||
expect(window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke).toBeDefined();
|
||||
|
||||
// Invoke through the bridge — should delegate to the native runtime
|
||||
await window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'wormhole.status',
|
||||
payload: undefined,
|
||||
});
|
||||
expect(nativeInvoke).toHaveBeenCalledTimes(1);
|
||||
expect(nativeInvoke.mock.calls[0][0]).toBe('wormhole.status');
|
||||
});
|
||||
|
||||
it('does not install bridge when no native runtime and shim env is off', async () => {
|
||||
// No __SHADOWBROKER_DESKTOP__ and NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM != '1'
|
||||
const originalEnv = process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM;
|
||||
process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM = '0';
|
||||
|
||||
const { bootstrapDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
const installed = bootstrapDesktopControlBridge();
|
||||
|
||||
expect(installed).toBe(false);
|
||||
expect(window.__SHADOWBROKER_LOCAL_CONTROL__).toBeUndefined();
|
||||
|
||||
process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM = originalEnv;
|
||||
});
|
||||
|
||||
it('falls back to HTTP shim when no native runtime and shim env is on', async () => {
|
||||
const originalEnv = process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM;
|
||||
process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM = '1';
|
||||
|
||||
const { bootstrapDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
const installed = bootstrapDesktopControlBridge();
|
||||
|
||||
expect(installed).toBe(true);
|
||||
// Bridge installed via shim
|
||||
expect(window.__SHADOWBROKER_LOCAL_CONTROL__).toBeDefined();
|
||||
// __SHADOWBROKER_DESKTOP__ is the HTTP-backed shim
|
||||
expect(window.__SHADOWBROKER_DESKTOP__).toBeDefined();
|
||||
expect(window.__SHADOWBROKER_DESKTOP__!.invokeLocalControl).toBeDefined();
|
||||
expect(window.__SHADOWBROKER_DESKTOP__!.getNativeControlAuditReport).toBeDefined();
|
||||
expect(window.__SHADOWBROKER_DESKTOP__!.clearNativeControlAuditReport).toBeDefined();
|
||||
|
||||
process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM = originalEnv;
|
||||
});
|
||||
|
||||
it('native runtime audit report is accessible through getDesktopNativeControlAuditReport', async () => {
|
||||
const auditReport = {
|
||||
totalEvents: 5,
|
||||
totalRecorded: 5,
|
||||
recent: [],
|
||||
byOutcome: { allowed: 5 },
|
||||
};
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: vi.fn().mockResolvedValue({}),
|
||||
getNativeControlAuditReport: () => auditReport,
|
||||
clearNativeControlAuditReport: vi.fn(),
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { bootstrapDesktopControlBridge, getDesktopNativeControlAuditReport } =
|
||||
await import('@/lib/desktopBridge');
|
||||
bootstrapDesktopControlBridge();
|
||||
|
||||
const report = getDesktopNativeControlAuditReport();
|
||||
expect(report).toEqual(auditReport);
|
||||
});
|
||||
|
||||
it('localControlFetch routes through native bridge when available', async () => {
|
||||
const nativeInvoke = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, status: 'connected' });
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: nativeInvoke,
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { installDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
installDesktopControlBridge(nativeRuntime);
|
||||
|
||||
const { localControlFetch } = await import('@/lib/localControlTransport');
|
||||
const response = await localControlFetch('/api/wormhole/status');
|
||||
const data = await response.json();
|
||||
|
||||
expect(nativeInvoke).toHaveBeenCalledTimes(1);
|
||||
expect(nativeInvoke.mock.calls[0][0]).toBe('wormhole.status');
|
||||
expect(data).toEqual({ ok: true, status: 'connected' });
|
||||
});
|
||||
|
||||
it('localControlFetch falls back to fetch when no bridge is present', async () => {
|
||||
// No bridge installed — localControlFetch should use regular fetch
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const { localControlFetch } = await import('@/lib/localControlTransport');
|
||||
await localControlFetch('/api/wormhole/status');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
const callUrl = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(callUrl).toContain('/api/wormhole/status');
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('Rust handler coverage matches the full contract command set', async () => {
|
||||
const { DESKTOP_CONTROL_COMMANDS } = await import(
|
||||
'@/lib/desktopControlContract'
|
||||
);
|
||||
// This test documents the expected contract size.
|
||||
// If the contract grows, the Rust handlers.rs must be updated to match.
|
||||
expect(DESKTOP_CONTROL_COMMANDS.length).toBe(27);
|
||||
|
||||
// Verify every command has a corresponding HTTP route
|
||||
const { commandToHttpRequest } = await import(
|
||||
'@/lib/desktopControlRouting'
|
||||
);
|
||||
for (const command of DESKTOP_CONTROL_COMMANDS) {
|
||||
// Gate commands need a payload with gate_id
|
||||
const payload = command.includes('gate')
|
||||
? { gate_id: 'test-gate', plaintext: 'x', reason: 'test', label: 'l', persona_id: 'p', epoch: 0, ciphertext: '', nonce: '', sender_ref: '', messages: [] }
|
||||
: undefined;
|
||||
expect(() => commandToHttpRequest(command, payload)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('native runtime forwards meta to invokeLocalControl', async () => {
|
||||
const nativeInvoke = vi.fn().mockResolvedValue({ ok: true });
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: nativeInvoke,
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { installDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
installDesktopControlBridge(nativeRuntime);
|
||||
|
||||
await window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
payload: { gate_id: 'infonet', reason: 'operator_reset' },
|
||||
meta: {
|
||||
capability: 'wormhole_gate_key',
|
||||
sessionProfileHint: 'gate_operator',
|
||||
enforceProfileHint: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(nativeInvoke).toHaveBeenCalledTimes(1);
|
||||
expect(nativeInvoke.mock.calls[0][0]).toBe('wormhole.gate.key.rotate');
|
||||
expect(nativeInvoke.mock.calls[0][1]).toEqual({ gate_id: 'infonet', reason: 'operator_reset' });
|
||||
// meta must not be dropped
|
||||
const receivedMeta = nativeInvoke.mock.calls[0][2];
|
||||
expect(receivedMeta).toBeDefined();
|
||||
expect(receivedMeta).toEqual(expect.objectContaining({
|
||||
capability: 'wormhole_gate_key',
|
||||
sessionProfileHint: 'gate_operator',
|
||||
enforceProfileHint: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('native runtime rejects on capability mismatch and records audit', async () => {
|
||||
const { controlCommandCapability } = await import('@/lib/desktopControlContract');
|
||||
const nativeInvoke = vi.fn().mockResolvedValue({ ok: true });
|
||||
const auditEntries: unknown[] = [];
|
||||
|
||||
// Simulate a Tauri-like runtime that checks capability mismatch
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: async (command, payload, meta) => {
|
||||
const expectedCap = controlCommandCapability(command!);
|
||||
if (meta?.capability && meta.capability !== expectedCap) {
|
||||
auditEntries.push({
|
||||
command,
|
||||
expectedCapability: expectedCap,
|
||||
declaredCapability: meta.capability,
|
||||
outcome: 'capability_mismatch',
|
||||
});
|
||||
throw new Error(
|
||||
`native_control_capability_mismatch:${meta.capability}:${expectedCap}`,
|
||||
);
|
||||
}
|
||||
return nativeInvoke(command, payload, meta);
|
||||
},
|
||||
getNativeControlAuditReport: () => ({
|
||||
totalEvents: auditEntries.length,
|
||||
totalRecorded: auditEntries.length,
|
||||
recent: [],
|
||||
byOutcome: { capability_mismatch: auditEntries.length },
|
||||
}),
|
||||
clearNativeControlAuditReport: vi.fn(),
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { installDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
installDesktopControlBridge(nativeRuntime);
|
||||
|
||||
// Declare wrong capability: 'settings' for a wormhole_gate_key command
|
||||
await expect(
|
||||
window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
payload: { gate_id: 'infonet', reason: 'test' },
|
||||
meta: { capability: 'settings' },
|
||||
}),
|
||||
).rejects.toThrow('native_control_capability_mismatch');
|
||||
|
||||
expect(nativeInvoke).not.toHaveBeenCalled();
|
||||
expect(auditEntries).toHaveLength(1);
|
||||
expect(auditEntries[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
declaredCapability: 'settings',
|
||||
expectedCapability: 'wormhole_gate_key',
|
||||
outcome: 'capability_mismatch',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('native runtime denies on profile enforcement and records audit', async () => {
|
||||
const { controlCommandCapability, sessionProfileCapabilities } = await import(
|
||||
'@/lib/desktopControlContract'
|
||||
);
|
||||
const nativeInvoke = vi.fn().mockResolvedValue({ ok: true });
|
||||
const auditEntries: unknown[] = [];
|
||||
|
||||
// Simulate a Tauri-like runtime that enforces session profiles
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: async (command, payload, meta) => {
|
||||
const expectedCap = controlCommandCapability(command!);
|
||||
const profile = meta?.sessionProfileHint;
|
||||
const profileCaps = profile ? sessionProfileCapabilities(profile) : [];
|
||||
const profileAllows =
|
||||
!profile || profileCaps.length === 0 || profileCaps.includes(expectedCap);
|
||||
const enforced = Boolean(meta?.enforceProfileHint && profile);
|
||||
if (!profileAllows && enforced) {
|
||||
auditEntries.push({
|
||||
command,
|
||||
expectedCapability: expectedCap,
|
||||
sessionProfile: profile,
|
||||
outcome: 'profile_denied',
|
||||
});
|
||||
throw new Error(
|
||||
`native_control_profile_mismatch:${profile}:${expectedCap}`,
|
||||
);
|
||||
}
|
||||
return nativeInvoke(command, payload, meta);
|
||||
},
|
||||
getNativeControlAuditReport: () => ({
|
||||
totalEvents: auditEntries.length,
|
||||
totalRecorded: auditEntries.length,
|
||||
recent: [],
|
||||
byOutcome: { profile_denied: auditEntries.length },
|
||||
}),
|
||||
clearNativeControlAuditReport: vi.fn(),
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { installDesktopControlBridge } = await import('@/lib/desktopBridge');
|
||||
installDesktopControlBridge(nativeRuntime);
|
||||
|
||||
// settings_only profile cannot access wormhole_gate_key commands
|
||||
await expect(
|
||||
window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
payload: { gate_id: 'infonet', reason: 'test' },
|
||||
meta: {
|
||||
capability: 'wormhole_gate_key',
|
||||
sessionProfileHint: 'settings_only',
|
||||
enforceProfileHint: true,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow('native_control_profile_mismatch');
|
||||
|
||||
expect(nativeInvoke).not.toHaveBeenCalled();
|
||||
expect(auditEntries).toHaveLength(1);
|
||||
expect(auditEntries[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
expectedCapability: 'wormhole_gate_key',
|
||||
sessionProfile: 'settings_only',
|
||||
outcome: 'profile_denied',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('native runtime audit report populates on allowed invocations', async () => {
|
||||
let auditCallCount = 0;
|
||||
const nativeInvoke = vi.fn().mockResolvedValue({ ok: true });
|
||||
const nativeRuntime: ShadowbrokerDesktopRuntime = {
|
||||
invokeLocalControl: async (command, payload, meta) => {
|
||||
auditCallCount++;
|
||||
return nativeInvoke(command, payload, meta);
|
||||
},
|
||||
getNativeControlAuditReport: () => ({
|
||||
totalEvents: auditCallCount,
|
||||
totalRecorded: auditCallCount,
|
||||
recent: [],
|
||||
byOutcome: { allowed: auditCallCount },
|
||||
}),
|
||||
clearNativeControlAuditReport: () => { auditCallCount = 0; },
|
||||
};
|
||||
window.__SHADOWBROKER_DESKTOP__ = nativeRuntime;
|
||||
|
||||
const { installDesktopControlBridge, getDesktopNativeControlAuditReport } =
|
||||
await import('@/lib/desktopBridge');
|
||||
installDesktopControlBridge(nativeRuntime);
|
||||
|
||||
await window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'wormhole.status',
|
||||
payload: undefined,
|
||||
});
|
||||
await window.__SHADOWBROKER_LOCAL_CONTROL__!.invoke!({
|
||||
command: 'settings.privacy.get',
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const report = getDesktopNativeControlAuditReport();
|
||||
expect(report).toBeDefined();
|
||||
expect(report!.totalEvents).toBe(2);
|
||||
expect(report!.totalRecorded).toBe(2);
|
||||
expect(report!.byOutcome).toEqual(expect.objectContaining({ allowed: 2 }));
|
||||
});
|
||||
|
||||
it('injected JS capability map covers every contract command', async () => {
|
||||
const { DESKTOP_CONTROL_COMMANDS, controlCommandCapability } = await import(
|
||||
'@/lib/desktopControlContract'
|
||||
);
|
||||
// The capability map embedded in the Tauri injected JS (main.rs) must
|
||||
// cover every command. This test verifies the TypeScript contract source
|
||||
// which the JS map mirrors — if the contract grows, this catches drift.
|
||||
for (const command of DESKTOP_CONTROL_COMMANDS) {
|
||||
const cap = controlCommandCapability(command);
|
||||
expect(cap).toBeDefined();
|
||||
expect(typeof cap).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('profile capability resolution matches between TS and expected Tauri JS tables', async () => {
|
||||
const { sessionProfileCapabilities } = await import(
|
||||
'@/lib/desktopControlContract'
|
||||
);
|
||||
// Verify the profile→capabilities mapping that the Tauri JS mirrors
|
||||
const profiles = [
|
||||
'full_app', 'gate_observe', 'gate_operator', 'wormhole_runtime', 'settings_only',
|
||||
] as const;
|
||||
for (const profile of profiles) {
|
||||
const caps = sessionProfileCapabilities(profile);
|
||||
expect(Array.isArray(caps)).toBe(true);
|
||||
expect(caps.length).toBeGreaterThan(0);
|
||||
}
|
||||
// Specific assertions matching the Tauri JS table
|
||||
expect(sessionProfileCapabilities('settings_only')).toEqual(['settings']);
|
||||
expect(sessionProfileCapabilities('gate_observe')).toEqual(['wormhole_gate_content']);
|
||||
expect(sessionProfileCapabilities('full_app')).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Tests for the desktop companion mode helper (desktopCompanion.ts).
|
||||
*
|
||||
* Validates runtime detection, Tauri invoke delegation, and browser-mode
|
||||
* fallback behavior without requiring a live Tauri runtime.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
isNativeDesktop,
|
||||
companionStatus,
|
||||
companionEnable,
|
||||
companionDisable,
|
||||
companionOpenBrowser,
|
||||
} from '@/lib/desktopCompanion';
|
||||
|
||||
const MOCK_STATUS = {
|
||||
enabled: false,
|
||||
url: null,
|
||||
warning: 'Browser companion mode is less secure than the native desktop window.',
|
||||
};
|
||||
|
||||
const MOCK_ENABLED = {
|
||||
enabled: true,
|
||||
url: 'http://127.0.0.1:3000',
|
||||
warning: 'Browser companion mode is less secure than the native desktop window.',
|
||||
};
|
||||
|
||||
describe('desktopCompanion', () => {
|
||||
afterEach(() => {
|
||||
// Clean up __TAURI__ mock
|
||||
delete (window as Record<string, unknown>).__TAURI__;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Runtime detection
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('isNativeDesktop', () => {
|
||||
it('returns false when __TAURI__ is not present', () => {
|
||||
expect(isNativeDesktop()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when __TAURI__.core.invoke is missing', () => {
|
||||
(window as Record<string, unknown>).__TAURI__ = { core: {} };
|
||||
expect(isNativeDesktop()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when __TAURI__.core.invoke is available', () => {
|
||||
(window as Record<string, unknown>).__TAURI__ = { core: { invoke: vi.fn() } };
|
||||
expect(isNativeDesktop()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Browser-mode fallback (all commands return null)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('browser mode (no Tauri)', () => {
|
||||
it('companionStatus returns null', async () => {
|
||||
expect(await companionStatus()).toBeNull();
|
||||
});
|
||||
|
||||
it('companionEnable returns null', async () => {
|
||||
expect(await companionEnable()).toBeNull();
|
||||
});
|
||||
|
||||
it('companionDisable returns null', async () => {
|
||||
expect(await companionDisable()).toBeNull();
|
||||
});
|
||||
|
||||
it('companionOpenBrowser returns null', async () => {
|
||||
expect(await companionOpenBrowser()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Desktop mode (mocked Tauri invoke)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('desktop mode (Tauri present)', () => {
|
||||
let mockInvoke: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockInvoke = vi.fn();
|
||||
(window as Record<string, unknown>).__TAURI__ = { core: { invoke: mockInvoke } };
|
||||
});
|
||||
|
||||
it('companionStatus invokes companion_status', async () => {
|
||||
mockInvoke.mockResolvedValue(MOCK_STATUS);
|
||||
const result = await companionStatus();
|
||||
expect(mockInvoke).toHaveBeenCalledWith('companion_status');
|
||||
expect(result).toEqual(MOCK_STATUS);
|
||||
});
|
||||
|
||||
it('companionEnable invokes companion_enable', async () => {
|
||||
mockInvoke.mockResolvedValue(MOCK_ENABLED);
|
||||
const result = await companionEnable();
|
||||
expect(mockInvoke).toHaveBeenCalledWith('companion_enable');
|
||||
expect(result).toEqual(MOCK_ENABLED);
|
||||
});
|
||||
|
||||
it('companionDisable invokes companion_disable', async () => {
|
||||
mockInvoke.mockResolvedValue(MOCK_STATUS);
|
||||
const result = await companionDisable();
|
||||
expect(mockInvoke).toHaveBeenCalledWith('companion_disable');
|
||||
expect(result).toEqual(MOCK_STATUS);
|
||||
});
|
||||
|
||||
it('companionOpenBrowser invokes companion_open_browser', async () => {
|
||||
mockInvoke.mockResolvedValue(MOCK_ENABLED);
|
||||
const result = await companionOpenBrowser();
|
||||
expect(mockInvoke).toHaveBeenCalledWith('companion_open_browser');
|
||||
expect(result).toEqual(MOCK_ENABLED);
|
||||
});
|
||||
|
||||
it('propagates Tauri invoke errors', async () => {
|
||||
mockInvoke.mockRejectedValue(new Error('companion_not_enabled'));
|
||||
await expect(companionOpenBrowser()).rejects.toThrow('companion_not_enabled');
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Status shape
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('CompanionStatus shape', () => {
|
||||
it('disabled status has null url', () => {
|
||||
expect(MOCK_STATUS.enabled).toBe(false);
|
||||
expect(MOCK_STATUS.url).toBeNull();
|
||||
expect(MOCK_STATUS.warning).toBeTruthy();
|
||||
});
|
||||
|
||||
it('enabled status has a url', () => {
|
||||
expect(MOCK_ENABLED.enabled).toBe(true);
|
||||
expect(MOCK_ENABLED.url).toBe('http://127.0.0.1:3000');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
describeNativeControlError,
|
||||
extractNativeGateResyncTarget,
|
||||
extractGateTargetRef,
|
||||
} from '../../lib/desktopControlContract';
|
||||
|
||||
@@ -22,6 +23,12 @@ describe('extractGateTargetRef', () => {
|
||||
expect(extractGateTargetRef('wormhole.gate.proof', { gate_id: 'alpha' })).toBe('alpha');
|
||||
});
|
||||
|
||||
it('extracts gate_id from gate state resync payload', () => {
|
||||
expect(extractGateTargetRef('wormhole.gate.state.resync', { gate_id: 'alpha' })).toBe(
|
||||
'alpha',
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts gate_id from gate message post payload', () => {
|
||||
expect(
|
||||
extractGateTargetRef('wormhole.gate.message.post', { gate_id: 'ops', plaintext: 'hi' }),
|
||||
@@ -86,9 +93,30 @@ describe('describeNativeControlError', () => {
|
||||
expect(describeNativeControlError(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('describes native gate resync requirement errors', () => {
|
||||
expect(
|
||||
describeNativeControlError('native_gate_state_resync_required:ops'),
|
||||
).toContain('gate resync');
|
||||
});
|
||||
|
||||
it('handles plain string errors', () => {
|
||||
expect(
|
||||
describeNativeControlError('native_control_profile_mismatch:foo'),
|
||||
).toContain('Denied');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractNativeGateResyncTarget', () => {
|
||||
it('extracts the gate id from native resync-required errors', () => {
|
||||
expect(extractNativeGateResyncTarget('native_gate_state_resync_required:ops')).toBe('ops');
|
||||
expect(extractNativeGateResyncTarget(new Error('native_gate_state_resync_required:infonet'))).toBe(
|
||||
'infonet',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for unrelated errors', () => {
|
||||
expect(extractNativeGateResyncTarget(new Error('network_error'))).toBeNull();
|
||||
expect(extractNativeGateResyncTarget('native_control_profile_mismatch:foo')).toBeNull();
|
||||
expect(extractNativeGateResyncTarget(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ describe('desktopControlRouting', () => {
|
||||
path: '/api/wormhole/gate/infonet/key',
|
||||
method: 'GET',
|
||||
});
|
||||
expect(commandToHttpRequest('wormhole.gate.state.resync', { gate_id: 'infonet' })).toEqual({
|
||||
path: '/api/wormhole/gate/state/export',
|
||||
method: 'POST',
|
||||
payload: { gate_id: 'infonet' },
|
||||
});
|
||||
expect(commandToHttpRequest('settings.news.reset')).toEqual({
|
||||
path: '/api/settings/news-feeds/reset',
|
||||
method: 'POST',
|
||||
@@ -27,11 +32,12 @@ describe('desktopControlRouting', () => {
|
||||
commandToHttpRequest('wormhole.gate.message.post', {
|
||||
gate_id: 'ops',
|
||||
plaintext: 'hello',
|
||||
reply_to: 'evt-parent-1',
|
||||
}),
|
||||
).toEqual({
|
||||
path: '/api/wormhole/gate/message/post',
|
||||
method: 'POST',
|
||||
payload: { gate_id: 'ops', plaintext: 'hello' },
|
||||
payload: { gate_id: 'ops', plaintext: 'hello', reply_to: 'evt-parent-1' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,8 +59,18 @@ describe('desktopControlRouting', () => {
|
||||
JSON.stringify({ gate_id: 'infonet', reason: 'operator_reset' }),
|
||||
),
|
||||
).toEqual({
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
payload: { gate_id: 'infonet', reason: 'operator_reset' },
|
||||
command: 'wormhole.gate.key.rotate',
|
||||
payload: { gate_id: 'infonet', reason: 'operator_reset' },
|
||||
});
|
||||
expect(
|
||||
httpRequestToInvokeRequest(
|
||||
'/api/wormhole/gate/state/export',
|
||||
'POST',
|
||||
JSON.stringify({ gate_id: 'infonet' }),
|
||||
),
|
||||
).toEqual({
|
||||
command: 'wormhole.gate.state.resync',
|
||||
payload: { gate_id: 'infonet' },
|
||||
});
|
||||
expect(
|
||||
httpRequestToInvokeRequest(
|
||||
@@ -96,6 +112,16 @@ describe('desktopControlRouting', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
httpRequestToInvokeRequest(
|
||||
'/api/wormhole/gate/message/post',
|
||||
'POST',
|
||||
JSON.stringify({ gate_id: 'ops', plaintext: 'hello', reply_to: 'evt-parent-2' }),
|
||||
),
|
||||
).toEqual({
|
||||
command: 'wormhole.gate.message.post',
|
||||
payload: { gate_id: 'ops', plaintext: 'hello', reply_to: 'evt-parent-2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for unsupported paths', () => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Tests for native desktop protected-settings readiness bypass.
|
||||
*
|
||||
* Verifies that:
|
||||
* - isNativeProtectedSettingsReady() correctly reflects native bridge presence
|
||||
* - When the native bridge is present, admin-session browser flow is bypassed
|
||||
* - When no native bridge, existing admin-session gating is preserved
|
||||
*
|
||||
* These are unit tests for the extracted readiness logic. They do NOT render
|
||||
* SettingsPanel — they test the decision layer that SettingsPanel depends on.
|
||||
* Full component render coverage is not claimed.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock the bridge detection used by nativeProtectedSettings
|
||||
const mockHasLocalControlBridge = vi.fn();
|
||||
|
||||
vi.mock('@/lib/localControlTransport', () => ({
|
||||
hasLocalControlBridge: () => mockHasLocalControlBridge(),
|
||||
canInvokeLocalControl: vi.fn(),
|
||||
localControlFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock adminSession to verify it's bypassed or called as expected
|
||||
const mockHasAdminSession = vi.fn();
|
||||
const mockPrimeAdminSession = vi.fn();
|
||||
|
||||
vi.mock('@/lib/adminSession', () => ({
|
||||
hasAdminSession: () => mockHasAdminSession(),
|
||||
primeAdminSession: (...args: unknown[]) => mockPrimeAdminSession(...args),
|
||||
clearAdminSession: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('isNativeProtectedSettingsReady', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockHasLocalControlBridge.mockReset();
|
||||
});
|
||||
|
||||
it('returns true when native local-control bridge is present', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(true);
|
||||
const mod = await import('@/lib/nativeProtectedSettings');
|
||||
expect(mod.isNativeProtectedSettingsReady()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no native bridge (browser mode)', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(false);
|
||||
const mod = await import('@/lib/nativeProtectedSettings');
|
||||
expect(mod.isNativeProtectedSettingsReady()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('controlPlaneFetch admin-session bypass with native bridge', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockHasLocalControlBridge.mockReset();
|
||||
mockHasAdminSession.mockReset();
|
||||
mockPrimeAdminSession.mockReset();
|
||||
});
|
||||
|
||||
it('skips primeAdminSession when native bridge handles the request', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(true);
|
||||
// canInvokeLocalControl is mocked to be truthy via the mock setup
|
||||
const { canInvokeLocalControl, localControlFetch } = await import(
|
||||
'@/lib/localControlTransport'
|
||||
);
|
||||
(canInvokeLocalControl as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
||||
(localControlFetch as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const mod = await import('@/lib/controlPlane');
|
||||
await mod.controlPlaneFetch('/api/settings/api-keys', {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(mockPrimeAdminSession).not.toHaveBeenCalled();
|
||||
expect(localControlFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('still primes admin session in browser mode (no native bridge)', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(false);
|
||||
const { canInvokeLocalControl, localControlFetch } = await import(
|
||||
'@/lib/localControlTransport'
|
||||
);
|
||||
(canInvokeLocalControl as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
(localControlFetch as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
mockPrimeAdminSession.mockResolvedValue(undefined);
|
||||
|
||||
const mod = await import('@/lib/controlPlane');
|
||||
await mod.controlPlaneFetch('/api/settings/api-keys', {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(mockPrimeAdminSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('native protected-settings readiness in SettingsPanel context', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockHasLocalControlBridge.mockReset();
|
||||
mockHasAdminSession.mockReset();
|
||||
});
|
||||
|
||||
it('native bridge present: hasAdminSession is NOT called by refreshAdminSession logic', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(true);
|
||||
// The helper returns true — SettingsPanel's refreshAdminSession should
|
||||
// short-circuit and never call hasAdminSession()
|
||||
const mod = await import('@/lib/nativeProtectedSettings');
|
||||
expect(mod.isNativeProtectedSettingsReady()).toBe(true);
|
||||
// hasAdminSession should not have been called
|
||||
expect(mockHasAdminSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('no native bridge: hasAdminSession is the readiness source', async () => {
|
||||
mockHasLocalControlBridge.mockReturnValue(false);
|
||||
const mod = await import('@/lib/nativeProtectedSettings');
|
||||
expect(mod.isNativeProtectedSettingsReady()).toBe(false);
|
||||
// In this scenario, SettingsPanel would call hasAdminSession() — we
|
||||
// verify the helper returns false so the browser flow is used.
|
||||
mockHasAdminSession.mockResolvedValue(true);
|
||||
const adminMod = await import('@/lib/adminSession');
|
||||
const ready = await adminMod.hasAdminSession();
|
||||
expect(ready).toBe(true);
|
||||
expect(mockHasAdminSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
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.7',
|
||||
assets: [
|
||||
{ name: 'ShadowBroker_0.9.7_x64_en-US.msi', browser_download_url: 'https://example.test/windows.msi' },
|
||||
{ name: 'ShadowBroker_0.9.7_x64-setup.exe', browser_download_url: 'https://example.test/windows-setup.exe' },
|
||||
{ name: 'ShadowBroker_0.9.7_aarch64.dmg', browser_download_url: 'https://example.test/macos.dmg' },
|
||||
{ name: 'ShadowBroker_0.9.7_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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user