release: prepare v0.9.7

This commit is contained in:
BigBodyCobain
2026-05-01 22:56:50 -06:00
parent ea457f27da
commit 28b3bd5ebf
670 changed files with 187059 additions and 14005 deletions
@@ -0,0 +1,228 @@
/**
* Phase 5F-A: CSP nonce plumbing tests.
*
* Validates:
* 1. Nonce appears in document CSP header
* 2. Nonce differs across repeated requests
* 3. next.config.ts no longer owns a static CSP header
* 4. Middleware does not break API/static routes (matcher exclusion)
* 5. Google Fonts domains are preserved in CSP
* 6. Production CSP preserves required directives
*/
import { describe, expect, it } from 'vitest';
import { NextRequest } from 'next/server';
import { middleware, config as middlewareConfig } from '@/middleware';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Call middleware with a fake document request and return the response. */
function callMiddleware(path = '/') {
const req = new NextRequest(`http://localhost${path}`, { method: 'GET' });
return middleware(req);
}
/** Extract the CSP header string from a middleware response. */
function getCsp(path = '/'): string {
return callMiddleware(path).headers.get('Content-Security-Policy') ?? '';
}
/** Check whether the middleware matcher regex excludes a given path. */
function matcherExcludes(path: string): boolean {
const pattern = middlewareConfig.matcher[0];
// Next.js wraps the matcher in ^/<pattern>$ for path matching.
// We replicate the essential check: the negative-lookahead prefix groups.
const re = new RegExp(`^${pattern}$`);
// Strip leading '/' because the matcher pattern starts with '/'.
return !re.test(path);
}
// ---------------------------------------------------------------------------
// 1. Nonce appears in document CSP header
// ---------------------------------------------------------------------------
describe('nonce in CSP header', () => {
it('CSP header contains a nonce-<value> token in script-src', () => {
const csp = getCsp();
expect(csp).toMatch(/'nonce-[A-Za-z0-9+/=]+'/) ;
});
it('nonce value is a base64-encoded UUID', () => {
const csp = getCsp();
const match = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/);
expect(match).not.toBeNull();
const decoded = Buffer.from(match![1], 'base64').toString();
// crypto.randomUUID() produces 8-4-4-4-12 hex with dashes
expect(decoded).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('x-nonce request header is set on the response', () => {
const res = callMiddleware();
// NextResponse.next({ request: { headers } }) merges into request headers.
// The CSP nonce in the header must match the one forwarded to server components.
const csp = res.headers.get('Content-Security-Policy') ?? '';
const nonceInCsp = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
expect(nonceInCsp).toBeTruthy();
});
});
// ---------------------------------------------------------------------------
// 2. Nonce differs across repeated requests
// ---------------------------------------------------------------------------
describe('nonce uniqueness', () => {
it('two sequential requests produce different nonces', () => {
const csp1 = getCsp();
const csp2 = getCsp();
const nonce1 = csp1.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
const nonce2 = csp2.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
expect(nonce1).toBeTruthy();
expect(nonce2).toBeTruthy();
expect(nonce1).not.toBe(nonce2);
});
it('ten requests produce ten distinct nonces', () => {
const nonces = new Set<string>();
for (let i = 0; i < 10; i++) {
const csp = getCsp();
const nonce = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1];
expect(nonce).toBeTruthy();
nonces.add(nonce!);
}
expect(nonces.size).toBe(10);
});
});
// ---------------------------------------------------------------------------
// 3. next.config.ts no longer owns static CSP
// ---------------------------------------------------------------------------
describe('next.config.ts CSP removal', () => {
it('securityHeaders in next.config does not include Content-Security-Policy', async () => {
// Import the built config and inspect the headers callback.
const nextConfig = (await import('../../../next.config')).default;
const headerEntries = await nextConfig.headers!();
const allHeaders = headerEntries.flatMap(
(entry: { headers: { key: string; value: string }[] }) => entry.headers,
);
const cspHeaders = allHeaders.filter(
(h: { key: string }) => h.key.toLowerCase() === 'content-security-policy',
);
expect(cspHeaders).toHaveLength(0);
});
it('non-CSP security headers are still present', async () => {
const nextConfig = (await import('../../../next.config')).default;
const headerEntries = await nextConfig.headers!();
const allKeys = headerEntries
.flatMap(
(entry: { headers: { key: string; value: string }[] }) => entry.headers,
)
.map((h: { key: string }) => h.key);
expect(allKeys).toContain('Referrer-Policy');
expect(allKeys).toContain('X-Content-Type-Options');
expect(allKeys).toContain('X-Frame-Options');
});
});
// ---------------------------------------------------------------------------
// 4. Middleware does not break API/static routes
// ---------------------------------------------------------------------------
describe('middleware matcher exclusions', () => {
it('excludes /api paths', () => {
expect(matcherExcludes('/api/mesh/events')).toBe(true);
});
it('excludes /_next/static paths', () => {
expect(matcherExcludes('/_next/static/chunks/main.js')).toBe(true);
});
it('excludes /_next/image paths', () => {
expect(matcherExcludes('/_next/image?url=foo')).toBe(true);
});
it('excludes /favicon.ico', () => {
expect(matcherExcludes('/favicon.ico')).toBe(true);
});
it('includes document paths like /', () => {
expect(matcherExcludes('/')).toBe(false);
});
it('includes document paths like /dashboard', () => {
expect(matcherExcludes('/dashboard')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// 5. Google Fonts domains are preserved in CSP
// ---------------------------------------------------------------------------
describe('Google Fonts domains in CSP', () => {
it('style-src includes https://fonts.googleapis.com', () => {
const csp = getCsp();
expect(csp).toContain('https://fonts.googleapis.com');
});
it('font-src includes https://fonts.gstatic.com', () => {
const csp = getCsp();
expect(csp).toContain('https://fonts.gstatic.com');
});
});
// ---------------------------------------------------------------------------
// 6. Production CSP directive completeness
// ---------------------------------------------------------------------------
describe('production CSP directive completeness', () => {
const csp = getCsp();
it('has default-src self', () => {
expect(csp).toContain("default-src 'self'");
});
it('has script-src with nonce', () => {
expect(csp).toMatch(/script-src [^;]*'nonce-/);
});
it('has style-src with unsafe-inline and fonts.googleapis.com', () => {
expect(csp).toMatch(/style-src [^;]*'unsafe-inline'/);
expect(csp).toMatch(/style-src [^;]*https:\/\/fonts\.googleapis\.com/);
});
it('has worker-src self blob:', () => {
expect(csp).toContain("worker-src 'self' blob:");
});
it('has child-src self blob:', () => {
expect(csp).toContain("child-src 'self' blob:");
});
it('has img-src with self data: blob: https:', () => {
expect(csp).toContain("img-src 'self' data: blob: https:");
});
it('has connect-src with self', () => {
expect(csp).toMatch(/connect-src 'self'/);
});
it('has object-src none', () => {
expect(csp).toContain("object-src 'none'");
});
it('has frame-ancestors none', () => {
expect(csp).toContain("frame-ancestors 'none'");
});
it('has base-uri self', () => {
expect(csp).toContain("base-uri 'self'");
});
it('has form-action self', () => {
expect(csp).toContain("form-action 'self'");
});
});
@@ -0,0 +1,222 @@
/**
* Phase 5F-B: Production script-src unsafe-inline removal tests.
*
* Validates:
* 1. Production CSP omits script-src 'unsafe-inline'
* 2. Dev CSP retains 'unsafe-inline' and 'unsafe-eval'
* 3. Unchanged directives (style-src, font-src, worker-src, etc.) intact
* 4. API/static route exclusions remain intact
* 5. isDev is evaluated per-request (not cached at module load)
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NextRequest } from 'next/server';
import { middleware, config as middlewareConfig } from '@/middleware';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function callMiddleware(path = '/') {
const req = new NextRequest(`http://localhost${path}`, { method: 'GET' });
return middleware(req);
}
function getCsp(path = '/'): string {
return callMiddleware(path).headers.get('Content-Security-Policy') ?? '';
}
/** Extract a single CSP directive by name. */
function getDirective(name: string, csp?: string): string {
const full = csp ?? getCsp();
const re = new RegExp(`${name}\\s+([^;]+)`);
return re.exec(full)?.[1]?.trim() ?? '';
}
function matcherExcludes(path: string): boolean {
const pattern = middlewareConfig.matcher[0];
const re = new RegExp(`^${pattern}$`);
return !re.test(path);
}
// ---------------------------------------------------------------------------
// 1. Production CSP omits script-src 'unsafe-inline'
// ---------------------------------------------------------------------------
describe('production script-src hardening', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'production');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('production script-src does NOT contain unsafe-inline', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).not.toContain("'unsafe-inline'");
});
it('production script-src does NOT contain unsafe-eval', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).not.toContain("'unsafe-eval'");
});
it('production script-src contains nonce', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
});
it('production script-src contains self and blob:', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).toContain("'self'");
expect(scriptSrc).toContain('blob:');
});
it('production connect-src uses restricted set', () => {
const connectSrc = getDirective('connect-src');
expect(connectSrc).not.toContain('http://127.0.0.1:8000');
expect(connectSrc).not.toContain('http://127.0.0.1:8787');
expect(connectSrc).toContain("'self'");
expect(connectSrc).toContain('wss:');
expect(connectSrc).toContain('https:');
});
});
// ---------------------------------------------------------------------------
// 2. Dev CSP retains required dev allowances
// ---------------------------------------------------------------------------
describe('dev script-src allowances', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'development');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('dev script-src contains unsafe-inline', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).toContain("'unsafe-inline'");
});
it('dev script-src contains unsafe-eval', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).toContain("'unsafe-eval'");
});
it('dev script-src still contains nonce', () => {
const scriptSrc = getDirective('script-src');
expect(scriptSrc).toMatch(/'nonce-[A-Za-z0-9+/=]+'/);
});
it('dev connect-src includes localhost backends', () => {
const connectSrc = getDirective('connect-src');
expect(connectSrc).toContain('http://127.0.0.1:8000');
expect(connectSrc).toContain('http://127.0.0.1:8787');
});
});
// ---------------------------------------------------------------------------
// 3. Unchanged directives remain intact across both modes
// ---------------------------------------------------------------------------
describe('unchanged directives in production', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'production');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('style-src preserves unsafe-inline and Google Fonts', () => {
const styleSrc = getDirective('style-src');
expect(styleSrc).toContain("'unsafe-inline'");
expect(styleSrc).toContain('https://fonts.googleapis.com');
});
it('font-src preserves data: and fonts.gstatic.com', () => {
const fontSrc = getDirective('font-src');
expect(fontSrc).toContain('data:');
expect(fontSrc).toContain('https://fonts.gstatic.com');
});
it('worker-src self blob:', () => {
expect(getCsp()).toContain("worker-src 'self' blob:");
});
it('child-src self blob:', () => {
expect(getCsp()).toContain("child-src 'self' blob:");
});
it('img-src self data: blob: https:', () => {
expect(getCsp()).toContain("img-src 'self' data: blob: https:");
});
it('object-src none', () => {
expect(getCsp()).toContain("object-src 'none'");
});
it('frame-ancestors none', () => {
expect(getCsp()).toContain("frame-ancestors 'none'");
});
it('base-uri self', () => {
expect(getCsp()).toContain("base-uri 'self'");
});
it('form-action self', () => {
expect(getCsp()).toContain("form-action 'self'");
});
it('default-src self', () => {
expect(getCsp()).toContain("default-src 'self'");
});
});
// ---------------------------------------------------------------------------
// 4. API/static route exclusions remain intact
// ---------------------------------------------------------------------------
describe('matcher exclusions unchanged', () => {
it('excludes /api paths', () => {
expect(matcherExcludes('/api/mesh/events')).toBe(true);
});
it('excludes /_next/static paths', () => {
expect(matcherExcludes('/_next/static/chunks/main.js')).toBe(true);
});
it('excludes /_next/image paths', () => {
expect(matcherExcludes('/_next/image?url=foo')).toBe(true);
});
it('excludes /favicon.ico', () => {
expect(matcherExcludes('/favicon.ico')).toBe(true);
});
it('includes document paths', () => {
expect(matcherExcludes('/')).toBe(false);
expect(matcherExcludes('/dashboard')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// 5. isDev evaluated per-request (not cached at module load)
// ---------------------------------------------------------------------------
describe('per-request environment evaluation', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('switching NODE_ENV between calls changes script-src', () => {
vi.stubEnv('NODE_ENV', 'production');
const prodScriptSrc = getDirective('script-src');
expect(prodScriptSrc).not.toContain("'unsafe-inline'");
vi.stubEnv('NODE_ENV', 'development');
const devScriptSrc = getDirective('script-src');
expect(devScriptSrc).toContain("'unsafe-inline'");
});
});
@@ -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);
});
});
});
@@ -0,0 +1,160 @@
/**
* Sprint 4D behavioral tests — MaplibreViewer CCTV proxy, subscription isolation,
* and parent-owned interpolation.
*
* These tests exercise actual runtime logic:
* 1. buildCctvProxyUrl — proxy construction with various URL inputs
* 2. Popup components do not own keyed subscriptions or map lifecycle
* 3. Parent-owned interpolation: ShipPopup receives pre-interpolated coords
*/
import { describe, expect, it } from 'vitest';
import { buildCctvProxyUrl } from '@/lib/cctvProxy';
import * as fs from 'fs';
import * as path from 'path';
const POPUP_DIR = path.resolve(__dirname, '../../components/MaplibreViewer/popups');
const COMP_DIR = path.resolve(__dirname, '../../components');
function readPopup(name: string): string {
return fs.readFileSync(path.join(POPUP_DIR, name), 'utf-8');
}
// ─── buildCctvProxyUrl runtime behavior ───────────────────────────────────
describe('MaplibreViewer behavior — buildCctvProxyUrl', () => {
it('proxies http:// URLs through /api/cctv/media', () => {
const result = buildCctvProxyUrl('http://example.com/stream.mjpg');
expect(result).toBe('/api/cctv/media?url=http%3A%2F%2Fexample.com%2Fstream.mjpg');
});
it('proxies https:// URLs through /api/cctv/media', () => {
const result = buildCctvProxyUrl('https://cdn.dot.gov/cam/42.m3u8');
expect(result).toBe('/api/cctv/media?url=https%3A%2F%2Fcdn.dot.gov%2Fcam%2F42.m3u8');
});
it('passes through relative URLs unchanged', () => {
expect(buildCctvProxyUrl('/local/stream.mp4')).toBe('/local/stream.mp4');
});
it('passes through empty string unchanged', () => {
expect(buildCctvProxyUrl('')).toBe('');
});
it('passes through data: URIs unchanged', () => {
expect(buildCctvProxyUrl('data:image/png;base64,abc')).toBe('data:image/png;base64,abc');
});
it('correctly encodes special characters in URLs', () => {
const url = 'http://cam.example.com/view?id=42&token=a b';
const result = buildCctvProxyUrl(url);
expect(result).toContain('/api/cctv/media?url=');
// Decode and verify roundtrip
const encoded = result.replace('/api/cctv/media?url=', '');
expect(decodeURIComponent(encoded)).toBe(url);
});
it('handles URLs with fragments and query params', () => {
const url = 'https://cam.example.com/stream#t=0?quality=hd';
const result = buildCctvProxyUrl(url);
expect(result).toContain('/api/cctv/media?url=');
const encoded = result.replace('/api/cctv/media?url=', '');
expect(decodeURIComponent(encoded)).toBe(url);
});
});
// ─── MaplibreViewer wiring — uses buildCctvProxyUrl ───────────────────────
describe('MaplibreViewer behavior — CCTV proxy wiring', () => {
const viewer = fs.readFileSync(path.join(COMP_DIR, 'MaplibreViewer.tsx'), 'utf-8');
it('MaplibreViewer calls buildCctvProxyUrl(rawUrl) in CCTV section', () => {
expect(viewer).toContain('buildCctvProxyUrl(rawUrl)');
});
it('MaplibreViewer imports buildCctvProxyUrl from @/lib/cctvProxy', () => {
expect(viewer).toMatch(
/import\s*\{[^}]*buildCctvProxyUrl[^}]*\}\s*from\s+['"]@\/lib\/cctvProxy['"]/,
);
});
it('CCTV proxy URL is assigned to `url` and passed to CctvFullscreenModal', () => {
const cctvSection = viewer.slice(
viewer.indexOf("selectedEntity?.type === 'cctv'"),
viewer.indexOf("selectedEntity?.type === 'cctv'") + 1600,
);
expect(cctvSection).toContain('const url = buildCctvProxyUrl(rawUrl)');
expect(cctvSection).toContain('url={url}');
expect(cctvSection).toContain('<CctvFullscreenModal');
});
});
// ─── Popup subscription isolation ─────────────────────────────────────────
describe('MaplibreViewer behavior — popup components have no keyed subscriptions', () => {
const popupFiles = [
'SatellitePopup.tsx',
'ShipPopup.tsx',
'SigintPopup.tsx',
'MilitaryBasePopup.tsx',
'RegionDossierPanel.tsx',
];
const FORBIDDEN_HOOKS = [
'useDataKeys',
'useDataSnapshot',
'useDataStore',
'useImperativeSource',
'useViewportBounds',
'useInterpolation',
];
for (const file of popupFiles) {
const name = path.basename(file, '.tsx');
it(`${name} does not import any data-store or map-lifecycle hooks`, () => {
const content = readPopup(file);
for (const hook of FORBIDDEN_HOOKS) {
expect(content).not.toContain(hook);
}
});
it(`${name} does not reference mapRef or mapInitRef`, () => {
const content = readPopup(file);
expect(content).not.toContain('mapRef');
expect(content).not.toContain('mapInitRef');
});
}
});
// ─── Parent-owned interpolation for popup positions ───────────────────────
describe('MaplibreViewer behavior — parent-owned interpolation feeds popup coords', () => {
const viewer = fs.readFileSync(path.join(COMP_DIR, 'MaplibreViewer.tsx'), 'utf-8');
it('MaplibreViewer calls interpShip before passing coords to ShipPopup', () => {
// Find the ship popup section
const shipSection = viewer.slice(
viewer.indexOf('{/* Ship / carrier click popup */}'),
viewer.indexOf('{/* Ship / carrier click popup */}') + 800,
);
// interpShip must be called, and its result fed into ShipPopup props
expect(shipSection).toContain('interpShip(ship)');
expect(shipSection).toContain('longitude={iLng}');
expect(shipSection).toContain('latitude={iLat}');
});
it('ShipPopup receives longitude and latitude as props (not computing them)', () => {
const shipPopup = readPopup('ShipPopup.tsx');
expect(shipPopup).toContain('longitude: number');
expect(shipPopup).toContain('latitude: number');
// Must NOT contain interpolation logic
expect(shipPopup).not.toContain('interpolatePosition');
expect(shipPopup).not.toContain('interpShip');
expect(shipPopup).not.toContain('useInterpolation');
});
it('MaplibreViewer owns useInterpolation hook', () => {
expect(viewer).toContain('useInterpolation');
expect(viewer).toMatch(/interpShip/);
expect(viewer).toMatch(/interpFlight/);
});
});
@@ -0,0 +1,371 @@
/**
* Sprint 4C regression tests — MaplibreViewer decomposition boundary checks.
*
* These tests validate the frozen contract for MaplibreViewer decomposition:
* 1. CctvFullscreenModal extracted to MaplibreViewer-local module
* 2. Popup components extracted to MaplibreViewer/popups/
* 3. CCTV proxy URL construction stays in MaplibreViewer (not in CctvFullscreenModal)
* 4. Popup components receive explicit props (not parent-scope captures)
* 5. Selection/dismissal: entity click → onClose dispatches onEntityClick(null)
* 6. MaplibreViewer retains <Map>, mapRef, useImperativeSource, Source/Layer, useViewportBounds
* 7. No keyed subscription regression (useDataKeys, not useDataSnapshot)
* 8. No mega-hook extraction (no useMapController)
*/
import { describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
const COMP_DIR = path.resolve(__dirname, '../../components');
function readComp(name: string): string {
return fs.readFileSync(path.join(COMP_DIR, name), 'utf-8');
}
// ─── CctvFullscreenModal extraction ────────────────────────────────────────
describe('MaplibreViewer decomposition — CctvFullscreenModal extraction', () => {
it('CctvFullscreenModal is defined in its own MaplibreViewer-local module', () => {
const modal = readComp('MaplibreViewer/CctvFullscreenModal.tsx');
expect(modal).toMatch(/export\s+function\s+CctvFullscreenModal/);
expect(modal).toContain('onClose');
});
it('CctvFullscreenModal exports CctvFullscreenModalProps interface', () => {
const modal = readComp('MaplibreViewer/CctvFullscreenModal.tsx');
expect(modal).toMatch(/export\s+interface\s+CctvFullscreenModalProps/);
expect(modal).toContain('url: string');
expect(modal).toContain('mediaType: string');
expect(modal).toContain('isVideo: boolean');
expect(modal).toContain('cameraName: string');
expect(modal).toContain('sourceAgency: string');
expect(modal).toContain('cameraId: string');
});
it('MaplibreViewer imports CctvFullscreenModal from extracted module', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).toMatch(
/import\s*\{.*CctvFullscreenModal.*\}\s*from\s+['"]@\/components\/MaplibreViewer\/CctvFullscreenModal['"]/,
);
});
it('MaplibreViewer no longer defines CctvFullscreenModal inline', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).not.toMatch(/^function\s+CctvFullscreenModal\s*\(/m);
});
it('CctvFullscreenModal does NOT contain proxy URL logic (stays in MaplibreViewer)', () => {
const modal = readComp('MaplibreViewer/CctvFullscreenModal.tsx');
// Proxy construction (/api/cctv/media?url=) must stay in MaplibreViewer
expect(modal).not.toContain('/api/cctv/media');
expect(modal).not.toContain('encodeURIComponent');
});
});
// ─── CCTV proxy URL behavior ───────────────────────────────────────────────
describe('MaplibreViewer decomposition — CCTV proxy URL behavior', () => {
const viewer = readComp('MaplibreViewer.tsx');
it('CCTV section delegates proxy URL construction to buildCctvProxyUrl', () => {
expect(viewer).toContain('buildCctvProxyUrl(rawUrl)');
expect(viewer).toMatch(
/import\s*\{[^}]*buildCctvProxyUrl[^}]*\}\s*from\s+['"]@\/lib\/cctvProxy['"]/,
);
});
it('CCTV section passes proxied URL to CctvFullscreenModal', () => {
// The pattern: url={url} where url is the proxied URL
const cctvSection = viewer.slice(
viewer.indexOf("selectedEntity?.type === 'cctv'"),
viewer.indexOf('</CctvFullscreenModal>') !== -1
? viewer.indexOf('</CctvFullscreenModal>')
: viewer.indexOf('/>', viewer.indexOf('<CctvFullscreenModal')) + 2,
);
expect(cctvSection).toContain('<CctvFullscreenModal');
expect(cctvSection).toContain('url={url}');
});
});
// ─── Popup explicit props ──────────────────────────────────────────────────
describe('MaplibreViewer decomposition — popup explicit props', () => {
it('SatellitePopup receives sat and onClose props', () => {
const popup = readComp('MaplibreViewer/popups/SatellitePopup.tsx');
expect(popup).toMatch(/export\s+interface\s+SatellitePopupProps/);
expect(popup).toContain('sat: Satellite');
expect(popup).toContain('onClose: () => void');
});
it('ShipPopup receives ship, longitude, latitude, onClose props', () => {
const popup = readComp('MaplibreViewer/popups/ShipPopup.tsx');
expect(popup).toMatch(/export\s+interface\s+ShipPopupProps/);
expect(popup).toContain('ship: Ship');
expect(popup).toContain('longitude: number');
expect(popup).toContain('latitude: number');
expect(popup).toContain('onClose: () => void');
});
it('SigintPopup receives data, lat, lng, kiwisdrs, setTrackedSdr, onClose props', () => {
const popup = readComp('MaplibreViewer/popups/SigintPopup.tsx');
expect(popup).toMatch(/export\s+interface\s+SigintPopupProps/);
expect(popup).toContain('data: SigintData');
expect(popup).toContain('lat: number');
expect(popup).toContain('lng: number');
expect(popup).toContain('kiwisdrs: KiwiSDR[]');
expect(popup).toContain('setTrackedSdr');
expect(popup).toContain('onClose: () => void');
});
it('MilitaryBasePopup receives base, oracleIntel, onClose props', () => {
const popup = readComp('MaplibreViewer/popups/MilitaryBasePopup.tsx');
expect(popup).toMatch(/export\s+interface\s+MilitaryBasePopupProps/);
expect(popup).toContain('base: MilitaryBase');
expect(popup).toContain('oracleIntel');
expect(popup).toContain('onClose: () => void');
});
it('RegionDossierPanel receives sentinel2, lat, lng, onClose props', () => {
const popup = readComp('MaplibreViewer/popups/RegionDossierPanel.tsx');
expect(popup).toMatch(/export\s+interface\s+RegionDossierPanelProps/);
expect(popup).toContain('sentinel2: Sentinel2Data');
expect(popup).toContain('lat: number');
expect(popup).toContain('lng: number');
expect(popup).toContain('onClose: () => void');
});
it('SigintPopup imports SigintSendForm and MeshtasticChannelFeed from SigintPanels', () => {
const popup = readComp('MaplibreViewer/popups/SigintPopup.tsx');
expect(popup).toMatch(
/import\s*\{[^}]*SigintSendForm[^}]*\}\s*from\s+['"]@\/components\/map\/panels\/SigintPanels['"]/,
);
expect(popup).toMatch(
/import\s*\{[^}]*MeshtasticChannelFeed[^}]*\}\s*from\s+['"]@\/components\/map\/panels\/SigintPanels['"]/,
);
});
it('SigintPopup computes nearestSdr internally (not passed from parent)', () => {
const popup = readComp('MaplibreViewer/popups/SigintPopup.tsx');
expect(popup).toContain('findNearestSdr');
});
});
// ─── Selection / dismissal behavior ────────────────────────────────────────
describe('MaplibreViewer decomposition — selection and dismissal', () => {
const viewer = readComp('MaplibreViewer.tsx');
it('satellite popup calls onEntityClick(null) on close', () => {
const satSection = viewer.slice(
viewer.indexOf("selectedEntity?.type === 'satellite'"),
viewer.indexOf("selectedEntity?.type === 'satellite'") + 500,
);
expect(satSection).toContain('<SatellitePopup');
expect(satSection).toContain('onClose={() => onEntityClick?.(null)}');
});
it('ship popup calls onEntityClick(null) on close', () => {
const shipSection = viewer.slice(
viewer.indexOf('{/* Ship / carrier click popup */}'),
viewer.indexOf('{/* Ship / carrier click popup */}') + 800,
);
expect(shipSection).toContain('<ShipPopup');
expect(shipSection).toContain('onClose={() => onEntityClick?.(null)}');
});
it('sigint popup calls onEntityClick(null) on close', () => {
const sigintSection = viewer.slice(
viewer.indexOf('{/* SIGINT signal click popup */}'),
viewer.indexOf('{/* SIGINT signal click popup */}') + 1200,
);
expect(sigintSection).toContain('<SigintPopup');
expect(sigintSection).toContain('onClose={() => onEntityClick?.(null)}');
});
it('military base popup calls onEntityClick(null) on close', () => {
const milSection = viewer.slice(
viewer.indexOf("selectedEntity?.type === 'military_base'"),
viewer.indexOf("selectedEntity?.type === 'military_base'") + 600,
);
expect(milSection).toContain('<MilitaryBasePopup');
expect(milSection).toContain('onClose={() => onEntityClick?.(null)}');
});
it('region dossier panel calls onEntityClick(null) on close', () => {
const rdSection = viewer.slice(
viewer.indexOf('{/* SENTINEL-2 IMAGERY'),
viewer.indexOf('{/* SENTINEL-2 IMAGERY') + 500,
);
expect(rdSection).toContain('<RegionDossierPanel');
expect(rdSection).toContain('onClose={() => onEntityClick(null)}');
});
it('CCTV fullscreen modal calls onEntityClick(null) on close', () => {
const cctvSection = viewer.slice(
viewer.indexOf("selectedEntity?.type === 'cctv'"),
viewer.indexOf("selectedEntity?.type === 'cctv'") + 1600,
);
expect(cctvSection).toContain('<CctvFullscreenModal');
expect(cctvSection).toContain('onClose={() => onEntityClick(null)}');
});
});
// ─── MaplibreViewer retains core responsibilities ──────────────────────────
describe('MaplibreViewer decomposition — retained core', () => {
const viewer = readComp('MaplibreViewer.tsx');
it('MaplibreViewer retains <Map> component', () => {
expect(viewer).toContain('<Map');
expect(viewer).toContain('</Map>');
});
it('MaplibreViewer retains mapRef', () => {
expect(viewer).toMatch(/mapRef\s*=\s*useRef/);
});
it('MaplibreViewer retains mapInitRef', () => {
expect(viewer).toMatch(/mapInitRef\s*=\s*useRef/);
});
it('MaplibreViewer retains initializeMap', () => {
expect(viewer).toContain('initializeMap');
});
it('MaplibreViewer retains useImperativeSource calls', () => {
expect(viewer).toContain('useImperativeSource');
});
it('MaplibreViewer retains Source and Layer declarations', () => {
expect(viewer).toContain('<Source');
expect(viewer).toContain('<Layer');
});
it('MaplibreViewer retains useViewportBounds', () => {
expect(viewer).toContain('useViewportBounds');
});
it('MaplibreViewer retains activeInteractiveLayerIds', () => {
expect(viewer).toContain('activeInteractiveLayerIds');
});
it('MaplibreViewer retains worker hooks', () => {
expect(viewer).toContain('useDynamicMapLayersWorker');
expect(viewer).toContain('useStaticMapLayersWorker');
});
});
// ─── No keyed subscription regression ──────────────────────────────────────
describe('MaplibreViewer decomposition — no keyed subscription regression', () => {
const viewer = readComp('MaplibreViewer.tsx');
it('MaplibreViewer uses useDataKeys (keyed subscription model)', () => {
expect(viewer).toContain('useDataKeys');
});
it('MaplibreViewer does NOT use useDataSnapshot', () => {
expect(viewer).not.toContain('useDataSnapshot');
});
it('MaplibreViewer imports useDataKeys from @/hooks/useDataStore', () => {
expect(viewer).toMatch(
/import\s*\{[^}]*useDataKeys[^}]*\}\s*from\s+['"]@\/hooks\/useDataStore['"]/,
);
});
});
// ─── No mega-hook extraction ───────────────────────────────────────────────
describe('MaplibreViewer decomposition — no mega-hook', () => {
it('no useMapController hook exists', () => {
const viewerDir = path.join(COMP_DIR, 'MaplibreViewer');
const files = fs.readdirSync(viewerDir, { recursive: true }) as string[];
const hookFiles = files.filter(
(f: string) => f.includes('useMapController') || f.includes('use-map-controller'),
);
expect(hookFiles).toHaveLength(0);
});
it('MaplibreViewer does not import useMapController', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).not.toContain('useMapController');
});
});
// ─── Popup components use Popup from react-map-gl ──────────────────────────
describe('MaplibreViewer decomposition — popup components own their Popup wrapper', () => {
const popupFiles = [
'MaplibreViewer/popups/SatellitePopup.tsx',
'MaplibreViewer/popups/ShipPopup.tsx',
'MaplibreViewer/popups/SigintPopup.tsx',
'MaplibreViewer/popups/MilitaryBasePopup.tsx',
];
for (const file of popupFiles) {
const name = path.basename(file, '.tsx');
it(`${name} imports Popup from react-map-gl/maplibre`, () => {
const content = readComp(file);
expect(content).toMatch(
/import\s*\{[^}]*Popup[^}]*\}\s*from\s+['"]react-map-gl\/maplibre['"]/,
);
});
it(`${name} renders a <Popup> component`, () => {
const content = readComp(file);
expect(content).toContain('<Popup');
});
}
it('RegionDossierPanel renders a fixed overlay (not a map Popup)', () => {
const content = readComp('MaplibreViewer/popups/RegionDossierPanel.tsx');
expect(content).not.toContain('<Popup');
expect(content).toContain("position: 'fixed'");
});
it('CctvFullscreenModal renders a fixed overlay (not a map Popup)', () => {
const content = readComp('MaplibreViewer/CctvFullscreenModal.tsx');
expect(content).not.toContain('<Popup');
expect(content).toContain("position: 'fixed'");
});
});
// ─── Data lookups stay in MaplibreViewer ────────────────────────────────────
describe('MaplibreViewer decomposition — data lookups in parent', () => {
it('satellite lookup stays in MaplibreViewer', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).toContain("data?.satellites?.find");
});
it('ship lookup stays in MaplibreViewer', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).toContain("data?.ships?.find");
});
it('sigint lookup stays in MaplibreViewer', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).toContain("data?.sigint?.find");
});
it('military_bases lookup stays in MaplibreViewer', () => {
const viewer = readComp('MaplibreViewer.tsx');
expect(viewer).toContain("data?.military_bases?.find");
});
it('popup components do NOT access data store directly', () => {
const popupFiles = [
'MaplibreViewer/popups/SatellitePopup.tsx',
'MaplibreViewer/popups/ShipPopup.tsx',
'MaplibreViewer/popups/SigintPopup.tsx',
'MaplibreViewer/popups/MilitaryBasePopup.tsx',
'MaplibreViewer/popups/RegionDossierPanel.tsx',
];
for (const file of popupFiles) {
const content = readComp(file);
expect(content).not.toContain('useDataKeys');
expect(content).not.toContain('useDataSnapshot');
expect(content).not.toContain('useDataStore');
}
});
});
@@ -0,0 +1,49 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
function readSource(relativePath: string): string {
const here = path.dirname(fileURLToPath(import.meta.url));
return fs.readFileSync(path.resolve(here, relativePath), 'utf-8');
}
describe('Sprint 6 DM compatibility sunset policy', () => {
it('keeps receive-side MeshChat request parsing off ambient legacy agent-id lookup', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toMatch(
/fetchDmPublicKey\(\s*API_BASE,\s*m\.sender_id,\s*senderContact\?\.invitePinnedPrekeyLookupHandle/s,
);
expect(controller).not.toMatch(
/fetchDmPublicKey\(\s*API_BASE,\s*m\.sender_id,[\s\S]{0,200}allowLegacyAgentId:\s*true/s,
);
});
it('keeps MessagesView receive-side contact parsing off ambient legacy agent-id lookup', () => {
const messagesView = readSource('../../components/InfonetTerminal/MessagesView.tsx');
expect(messagesView).toMatch(
/fetchDmPublicKey\(\s*API_BASE,\s*senderId,\s*existingContact\?\.invitePinnedPrekeyLookupHandle/s,
);
expect(messagesView).not.toMatch(
/fetchDmPublicKey\(\s*API_BASE,\s*senderId,[\s\S]{0,200}allowLegacyAgentId:\s*true/s,
);
});
it('keeps MeshTerminal legacy lookup limited to explicit migration commands', () => {
const terminal = readSource('../../components/MeshTerminal.tsx');
expect(terminal).not.toMatch(
/fetchDmPublicKey\(\s*API,\s*message\.sender_id,[\s\S]{0,200}allowLegacyAgentId:/s,
);
expect(terminal).not.toMatch(
/fetchDmPublicKey\(\s*API,\s*m\.sender_id,[\s\S]{0,200}allowLegacyAgentId:/s,
);
const legacyLookupMatches = terminal.match(/allowLegacyAgentId:\s*true/g) || [];
expect(legacyLookupMatches).toHaveLength(1);
expect(terminal).toContain("only for legacy migration");
});
});
@@ -0,0 +1,237 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
function makeStorage() {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
};
}
describe('dmPollScheduler', () => {
beforeEach(() => {
vi.resetModules();
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
});
describe('jitteredPollDelay', () => {
it('returns a value within the default jitter band', async () => {
const { jitteredPollDelay } = await import('@/lib/dmPollScheduler');
const base = 12_000;
// r=0 → factor=0.8 → 9600; r=1 → factor=1.4 → 16800
expect(jitteredPollDelay(base, { profile: 'default', random: 0 })).toBe(9600);
expect(jitteredPollDelay(base, { profile: 'default', random: 1 })).toBe(16800);
});
it('high-privacy band is wider than default', async () => {
const { jitteredPollDelay } = await import('@/lib/dmPollScheduler');
const base = 12_000;
const defaultMin = jitteredPollDelay(base, { profile: 'default', random: 0 });
const defaultMax = jitteredPollDelay(base, { profile: 'default', random: 1 });
const highMin = jitteredPollDelay(base, { profile: 'high', random: 0 });
const highMax = jitteredPollDelay(base, { profile: 'high', random: 1 });
const defaultRange = defaultMax - defaultMin;
const highRange = highMax - highMin;
expect(highRange).toBeGreaterThan(defaultRange);
});
it('never returns the exact base interval across random inputs', async () => {
const { jitteredPollDelay } = await import('@/lib/dmPollScheduler');
const base = 12_000;
const samples = Array.from({ length: 100 }, (_, i) =>
jitteredPollDelay(base, { profile: 'default', random: i / 100 }),
);
// At most one value could accidentally equal base; the set should be diverse
const unique = new Set(samples);
expect(unique.size).toBeGreaterThan(50);
// The exact base value corresponds to r ≈ 0.333...; verify it's the only one
const exactBaseCount = samples.filter((v) => v === base).length;
expect(exactBaseCount).toBeLessThanOrEqual(1);
});
it('reads privacy profile from browser storage when no override', async () => {
sessionStorage.setItem('sb_privacy_profile', 'high');
const { jitteredPollDelay } = await import('@/lib/dmPollScheduler');
const base = 10_000;
// r=0 with high profile → factor=0.5 → 5000
expect(jitteredPollDelay(base, { random: 0 })).toBe(5000);
});
it('returns positive value for any base and profile', async () => {
const { jitteredPollDelay } = await import('@/lib/dmPollScheduler');
for (const profile of ['default', 'high', 'unknown']) {
for (const r of [0, 0.25, 0.5, 0.75, 1]) {
const delay = jitteredPollDelay(15_000, { profile, random: r });
expect(delay).toBeGreaterThan(0);
}
}
});
});
describe('catchUpDelay', () => {
it('returns a value within the default catch-up band', async () => {
const { catchUpDelay } = await import('@/lib/dmPollScheduler');
// default: min=2000, max=5000
expect(catchUpDelay({ profile: 'default', random: 0 })).toBe(2000);
expect(catchUpDelay({ profile: 'default', random: 1 })).toBe(5000);
});
it('high-privacy catch-up delay is longer than default', async () => {
const { catchUpDelay } = await import('@/lib/dmPollScheduler');
const defaultMid = catchUpDelay({ profile: 'default', random: 0.5 });
const highMid = catchUpDelay({ profile: 'high', random: 0.5 });
expect(highMid).toBeGreaterThan(defaultMid);
});
it('catch-up delay is always shorter than normal poll delay', async () => {
const { jitteredPollDelay, catchUpDelay } = await import('@/lib/dmPollScheduler');
// Worst-case catch-up (r=1, high) vs best-case normal poll (r=0, default, base=12000)
const maxCatchUp = catchUpDelay({ profile: 'high', random: 1 });
const minNormal = jitteredPollDelay(12_000, { profile: 'default', random: 0 });
expect(maxCatchUp).toBeLessThan(minNormal);
});
it('catch-up delay is never zero', async () => {
const { catchUpDelay } = await import('@/lib/dmPollScheduler');
for (const profile of ['default', 'high']) {
const delay = catchUpDelay({ profile, random: 0 });
expect(delay).toBeGreaterThan(0);
}
});
});
describe('MAX_CATCHUP_POLLS', () => {
it('is a small positive integer bounding catch-up bursts', async () => {
const { MAX_CATCHUP_POLLS } = await import('@/lib/dmPollScheduler');
expect(MAX_CATCHUP_POLLS).toBeGreaterThanOrEqual(1);
expect(MAX_CATCHUP_POLLS).toBeLessThanOrEqual(5);
});
});
describe('classifyTick', () => {
it('catch-up tick skips count refresh', async () => {
const { classifyTick } = await import('@/lib/dmPollScheduler');
const result = classifyTick(true, 3, 12_000, { profile: 'default', random: 0.5 });
expect(result.refreshCount).toBe(false);
expect(result.newBudget).toBe(2);
});
it('normal tick includes count refresh', async () => {
const { classifyTick } = await import('@/lib/dmPollScheduler');
const result = classifyTick(false, 3, 12_000, { profile: 'default', random: 0.5 });
expect(result.refreshCount).toBe(true);
});
it('budget exhaustion falls back to normal with count', async () => {
const { classifyTick } = await import('@/lib/dmPollScheduler');
// has_more=true but budget=0 → normal tick
const result = classifyTick(true, 0, 12_000, { profile: 'default', random: 0.5 });
expect(result.refreshCount).toBe(true);
});
it('budget resets after fallback to normal', async () => {
const { classifyTick, MAX_CATCHUP_POLLS } = await import('@/lib/dmPollScheduler');
const result = classifyTick(false, 1, 12_000, { profile: 'default', random: 0.5 });
expect(result.newBudget).toBe(MAX_CATCHUP_POLLS);
});
it('catch-up delay is used during catch-up ticks', async () => {
const { classifyTick, catchUpDelay } = await import('@/lib/dmPollScheduler');
const opts = { profile: 'default' as const, random: 0.5 };
const result = classifyTick(true, 2, 12_000, opts);
expect(result.delay).toBe(catchUpDelay(opts));
});
it('normal delay is used during normal ticks', async () => {
const { classifyTick, jitteredPollDelay } = await import('@/lib/dmPollScheduler');
const opts = { profile: 'default' as const, random: 0.5 };
const result = classifyTick(false, 3, 12_000, opts);
expect(result.delay).toBe(jitteredPollDelay(12_000, opts));
});
});
describe('scheduling contract', () => {
it('simulated poll loop uses classifyTick for cadence and count decisions', async () => {
const { classifyTick, catchUpDelay, jitteredPollDelay, MAX_CATCHUP_POLLS } =
await import('@/lib/dmPollScheduler');
const ticks: Array<{ delay: number; refreshCount: boolean }> = [];
let budget = MAX_CATCHUP_POLLS;
const hasMoreSequence = [true, true, true, true, false, false, true, false];
const opts = { profile: 'default' as const, random: 0.5 };
for (const hasMore of hasMoreSequence) {
const result = classifyTick(hasMore, budget, 12_000, opts);
budget = result.newBudget;
ticks.push({ delay: result.delay, refreshCount: result.refreshCount });
}
const catchUpValue = catchUpDelay(opts);
const normalValue = jitteredPollDelay(12_000, opts);
// First MAX_CATCHUP_POLLS catch-up ticks: short delay, no count
for (let i = 0; i < MAX_CATCHUP_POLLS; i++) {
expect(ticks[i].delay).toBe(catchUpValue);
expect(ticks[i].refreshCount).toBe(false);
}
// 4th has_more exceeds budget → normal with count
expect(ticks[MAX_CATCHUP_POLLS].delay).toBe(normalValue);
expect(ticks[MAX_CATCHUP_POLLS].refreshCount).toBe(true);
// Non-has_more ticks: normal with count
expect(ticks[4].delay).toBe(normalValue);
expect(ticks[4].refreshCount).toBe(true);
expect(ticks[5].delay).toBe(normalValue);
expect(ticks[5].refreshCount).toBe(true);
});
it('count is never refreshed during catch-up across a full backlog drain', async () => {
const { classifyTick, MAX_CATCHUP_POLLS } = await import('@/lib/dmPollScheduler');
let budget = MAX_CATCHUP_POLLS;
const countRefreshes: boolean[] = [];
// Simulate: has_more for exactly budget ticks, then two normal ticks
const hasMoreSequence = [
...Array(MAX_CATCHUP_POLLS).fill(true),
false,
false,
];
for (const hasMore of hasMoreSequence) {
const result = classifyTick(hasMore, budget, 12_000, { profile: 'default', random: 0.5 });
budget = result.newBudget;
countRefreshes.push(result.refreshCount);
}
// Catch-up ticks should not refresh count
for (let i = 0; i < MAX_CATCHUP_POLLS; i++) {
expect(countRefreshes[i]).toBe(false);
}
// Normal ticks after catch-up do refresh count
expect(countRefreshes[MAX_CATCHUP_POLLS]).toBe(true);
expect(countRefreshes[MAX_CATCHUP_POLLS + 1]).toBe(true);
});
it('no fixed cadence is reintroduced by classifyTick', async () => {
const { classifyTick } = await import('@/lib/dmPollScheduler');
const delays = new Set<number>();
for (let r = 0; r < 20; r++) {
const result = classifyTick(false, 3, 12_000, { profile: 'default', random: r / 20 });
delays.add(result.delay);
}
// All 20 random inputs should produce diverse delays, not a fixed value
expect(delays.size).toBeGreaterThan(10);
});
});
});
@@ -0,0 +1,30 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
describe('DM selftest client', () => {
beforeEach(() => {
controlPlaneJson.mockReset();
});
it('runs the local DM selftest without requiring an admin browser session', async () => {
controlPlaneJson.mockResolvedValue({ ok: true });
const { runWormholeDmSelftest } = await import('@/mesh/wormholeIdentityClient');
await runWormholeDmSelftest('probe');
expect(controlPlaneJson).toHaveBeenCalledWith(
'/api/wormhole/dm/selftest',
expect.objectContaining({
method: 'POST',
requireAdminSession: false,
body: JSON.stringify({ message: 'probe' }),
}),
);
});
});
@@ -0,0 +1,229 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
const hasLocalControlBridge = vi.fn(() => false);
const getGateSessionStreamAccessHeaders = vi.fn();
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
vi.mock('@/lib/localControlTransport', () => ({
hasLocalControlBridge,
}));
vi.mock('@/mesh/gateSessionStream', () => ({
getGateSessionStreamAccessHeaders,
}));
describe('gateAccessProof cache', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneJson.mockReset();
hasLocalControlBridge.mockReset();
hasLocalControlBridge.mockReturnValue(false);
getGateSessionStreamAccessHeaders.mockReset();
getGateSessionStreamAccessHeaders.mockReturnValue(undefined);
});
it('caches browser/web gate proofs just under the backend validity window', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T22:40:00.000Z'));
try {
controlPlaneJson.mockResolvedValue({
node_id: '!sb_gate',
ts: 1712345678,
proof: 'proof-a',
});
const mod = await import('@/mesh/gateAccessProof');
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': '1712345678',
});
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': '1712345678',
});
expect(controlPlaneJson).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(52_001);
await mod.buildGateAccessHeaders('finance');
expect(controlPlaneJson).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('uses a shorter proof cache window on native runtimes', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T22:40:00.000Z'));
try {
hasLocalControlBridge.mockReturnValue(true);
controlPlaneJson.mockResolvedValue({
node_id: '!sb_gate',
ts: 1712345678,
proof: 'proof-native',
});
const mod = await import('@/mesh/gateAccessProof');
await mod.buildGateAccessHeaders('finance');
vi.advanceTimersByTime(35_001);
await mod.buildGateAccessHeaders('finance');
expect(controlPlaneJson).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('coalesces concurrent proof requests for the same gate into one control-plane call', async () => {
let release: ((value: { node_id: string; ts: number; proof: string }) => void) | null = null;
controlPlaneJson.mockImplementation(
() =>
new Promise((resolve) => {
release = resolve as typeof release;
}),
);
const mod = await import('@/mesh/gateAccessProof');
const first = mod.buildGateAccessHeaders('finance');
const second = mod.buildGateAccessHeaders('finance');
expect(controlPlaneJson).toHaveBeenCalledTimes(1);
release?.({
node_id: '!sb_gate',
ts: 1712345678,
proof: 'proof-a',
});
await expect(first).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': '1712345678',
});
await expect(second).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': '1712345678',
});
});
it('uses stream bootstrap access headers before falling back to the gate proof endpoint', async () => {
getGateSessionStreamAccessHeaders.mockReturnValue({
'X-Wormhole-Node-Id': '!sb_stream',
'X-Wormhole-Gate-Proof': 'proof-stream',
'X-Wormhole-Gate-Ts': '1712345678',
});
const mod = await import('@/mesh/gateAccessProof');
await expect(mod.buildGateAccessHeaders('finance', { mode: 'session_stream' })).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_stream',
'X-Wormhole-Gate-Proof': 'proof-stream',
'X-Wormhole-Gate-Ts': '1712345678',
});
expect(getGateSessionStreamAccessHeaders).toHaveBeenCalledWith('finance');
expect(controlPlaneJson).not.toHaveBeenCalled();
});
it('reuses a fresh-enough proof longer for held wait requests than for ordinary reads', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T22:40:00.000Z'));
try {
const firstTs = Math.floor(Date.now() / 1000);
const secondTs = Math.floor((Date.now() + 55_000) / 1000);
controlPlaneJson
.mockResolvedValueOnce({
node_id: '!sb_gate',
ts: firstTs,
proof: 'proof-a',
})
.mockResolvedValueOnce({
node_id: '!sb_gate',
ts: secondTs,
proof: 'proof-b',
});
const mod = await import('@/mesh/gateAccessProof');
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': String(firstTs),
});
vi.advanceTimersByTime(55_000);
await expect(mod.buildGateAccessHeaders('finance', { mode: 'wait' })).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': String(firstTs),
});
expect(controlPlaneJson).toHaveBeenCalledTimes(1);
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-b',
'X-Wormhole-Gate-Ts': String(secondTs),
});
expect(controlPlaneJson).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('reuses a fresh-enough proof longer for session-stream refreshes than for ordinary reads', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T22:40:00.000Z'));
try {
const firstTs = Math.floor(Date.now() / 1000);
const secondTs = Math.floor((Date.now() + 55_000) / 1000);
controlPlaneJson
.mockResolvedValueOnce({
node_id: '!sb_gate',
ts: firstTs,
proof: 'proof-a',
})
.mockResolvedValueOnce({
node_id: '!sb_gate',
ts: secondTs,
proof: 'proof-b',
});
const mod = await import('@/mesh/gateAccessProof');
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': String(firstTs),
});
vi.advanceTimersByTime(55_000);
await expect(mod.buildGateAccessHeaders('finance', { mode: 'session_stream' })).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-a',
'X-Wormhole-Gate-Ts': String(firstTs),
});
expect(controlPlaneJson).toHaveBeenCalledTimes(1);
await expect(mod.buildGateAccessHeaders('finance')).resolves.toEqual({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof-b',
'X-Wormhole-Gate-Ts': String(secondTs),
});
expect(controlPlaneJson).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
});
@@ -0,0 +1,135 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hasLocalControlBridge = vi.fn(() => false);
vi.mock('@/lib/localControlTransport', () => ({
hasLocalControlBridge,
}));
describe('gateCatalogSnapshot cache', () => {
const fetchMock = vi.fn();
beforeEach(() => {
vi.resetModules();
fetchMock.mockReset();
hasLocalControlBridge.mockReset();
hasLocalControlBridge.mockReturnValue(false);
vi.stubGlobal('fetch', fetchMock);
});
it('coarsens browser/web gate catalog reads through a short shared cache window', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:10:00.000Z'));
try {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
gates: [{ gate_id: 'infonet', display_name: 'Infonet Commons' }],
}),
});
const mod = await import('@/mesh/gateCatalogSnapshot');
await expect(mod.fetchGateCatalogSnapshot()).resolves.toEqual([
{ gate_id: 'infonet', display_name: 'Infonet Commons' },
]);
await expect(mod.fetchGateCatalogSnapshot()).resolves.toEqual([
{ gate_id: 'infonet', display_name: 'Infonet Commons' },
]);
expect(fetchMock).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(18_001);
await mod.fetchGateCatalogSnapshot();
expect(fetchMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('uses a shorter cache window for native gate catalog/detail snapshots', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:10:00.000Z'));
try {
hasLocalControlBridge.mockReturnValue(true);
fetchMock
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gates: [{ gate_id: 'finance', display_name: 'Finance' }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gates: [{ gate_id: 'finance', display_name: 'Finance' }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gate_id: 'finance',
display_name: 'Finance',
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gate_id: 'finance',
display_name: 'Finance',
}),
});
const mod = await import('@/mesh/gateCatalogSnapshot');
await mod.fetchGateCatalogSnapshot();
vi.advanceTimersByTime(6_001);
await mod.fetchGateCatalogSnapshot();
await mod.fetchGateDetailSnapshot('finance');
vi.advanceTimersByTime(5_001);
await mod.fetchGateDetailSnapshot('finance');
expect(fetchMock).toHaveBeenCalledTimes(4);
} finally {
vi.useRealTimers();
}
});
it('invalidates cached gate detail snapshots explicitly', async () => {
fetchMock
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gate_id: 'infonet',
display_name: 'Infonet Commons',
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
gate_id: 'infonet',
display_name: 'Infonet Commons v2',
}),
});
const mod = await import('@/mesh/gateCatalogSnapshot');
await expect(mod.fetchGateDetailSnapshot('infonet')).resolves.toEqual(
expect.objectContaining({
gate_id: 'infonet',
display_name: 'Infonet Commons',
}),
);
mod.invalidateGateDetailSnapshot('infonet');
await expect(mod.fetchGateDetailSnapshot('infonet')).resolves.toEqual(
expect.objectContaining({
gate_id: 'infonet',
display_name: 'Infonet Commons v2',
}),
);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,737 @@
import '@testing-library/jest-dom/vitest';
import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
const mocks = vi.hoisted(() => ({
controlPlaneJson: vi.fn(),
approveGateCompatFallback: vi.fn(),
decryptWormholeGateMessages: vi.fn(),
fetchWormholeGateKeyStatus: vi.fn(),
hasGateCompatFallbackApproval: vi.fn(() => false),
postWormholeGateMessage: vi.fn(),
prepareWormholeInteractiveLane: vi.fn(async () => ({
ready: true,
settingsEnabled: true,
transportTier: 'private_transitional',
identity: null,
})),
revokeGateCompatFallback: vi.fn(),
syncBrowserWormholeGateState: vi.fn(async () => true),
getGateSessionStreamStatus: vi.fn(() => ({
enabled: false,
phase: 'idle',
transport: 'sse',
sessionId: '',
subscriptions: [],
heartbeatS: 0,
batchMs: 0,
lastEventType: '',
lastEventAt: 0,
detail: '',
})),
retainGateSessionStreamGate: vi.fn(() => vi.fn()),
subscribeGateSessionStreamEvents: vi.fn(() => vi.fn()),
subscribeGateSessionStreamStatus: vi.fn((listener: (status: unknown) => void) => {
listener(mocks.getGateSessionStreamStatus());
return vi.fn();
}),
getGateSessionStreamAccessHeaders: vi.fn(() => undefined),
getGateSessionStreamKeyStatus: vi.fn(() => null),
invalidateGateSessionStreamGateContext: vi.fn(),
}));
vi.mock('@/lib/api', () => ({
API_BASE: 'http://test.local',
}));
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson: mocks.controlPlaneJson,
}));
vi.mock('@/mesh/meshIdentity', () => ({
nextSequence: vi.fn(() => 1),
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
approveGateCompatFallback: mocks.approveGateCompatFallback,
decryptWormholeGateMessages: mocks.decryptWormholeGateMessages,
fetchWormholeGateKeyStatus: mocks.fetchWormholeGateKeyStatus,
hasGateCompatFallbackApproval: mocks.hasGateCompatFallbackApproval,
postWormholeGateMessage: mocks.postWormholeGateMessage,
prepareWormholeInteractiveLane: mocks.prepareWormholeInteractiveLane,
revokeGateCompatFallback: mocks.revokeGateCompatFallback,
signMeshEvent: vi.fn(),
syncBrowserWormholeGateState: mocks.syncBrowserWormholeGateState,
}));
vi.mock('@/mesh/gateEnvelope', () => ({
gateEnvelopeDisplayText: vi.fn(() => 'sealed'),
gateEnvelopeState: vi.fn(() => 'sealed'),
isEncryptedGateEnvelope: vi.fn((message: { ciphertext?: string }) => Boolean(message?.ciphertext)),
}));
vi.mock('@/mesh/meshSchema', () => ({
validateEventPayload: vi.fn(() => ({ ok: true })),
}));
vi.mock('@/hooks/useGateSSE', () => ({
useGateSSE: vi.fn(),
}));
vi.mock('@/mesh/gateSessionStream', () => ({
getGateSessionStreamAccessHeaders: mocks.getGateSessionStreamAccessHeaders,
getGateSessionStreamKeyStatus: mocks.getGateSessionStreamKeyStatus,
getGateSessionStreamStatus: mocks.getGateSessionStreamStatus,
invalidateGateSessionStreamGateContext: mocks.invalidateGateSessionStreamGateContext,
retainGateSessionStreamGate: mocks.retainGateSessionStreamGate,
subscribeGateSessionStreamEvents: mocks.subscribeGateSessionStreamEvents,
subscribeGateSessionStreamStatus: mocks.subscribeGateSessionStreamStatus,
}));
describe('GateView compat-decrypt UX', () => {
let streamStatusListeners: Array<(status: unknown) => void> = [];
beforeEach(() => {
streamStatusListeners = [];
mocks.controlPlaneJson.mockReset();
mocks.approveGateCompatFallback.mockReset();
mocks.decryptWormholeGateMessages.mockReset();
mocks.fetchWormholeGateKeyStatus.mockReset();
mocks.hasGateCompatFallbackApproval.mockReset();
mocks.hasGateCompatFallbackApproval.mockReturnValue(false);
mocks.postWormholeGateMessage.mockReset();
mocks.prepareWormholeInteractiveLane.mockReset();
mocks.prepareWormholeInteractiveLane.mockResolvedValue({
ready: true,
settingsEnabled: true,
transportTier: 'private_transitional',
identity: null,
});
mocks.revokeGateCompatFallback.mockReset();
mocks.syncBrowserWormholeGateState.mockReset();
mocks.getGateSessionStreamStatus.mockReset();
mocks.retainGateSessionStreamGate.mockReset();
mocks.subscribeGateSessionStreamEvents.mockReset();
mocks.subscribeGateSessionStreamStatus.mockReset();
mocks.getGateSessionStreamAccessHeaders.mockReset();
mocks.getGateSessionStreamAccessHeaders.mockReturnValue(undefined);
mocks.getGateSessionStreamKeyStatus.mockReset();
mocks.getGateSessionStreamKeyStatus.mockReturnValue(null);
mocks.invalidateGateSessionStreamGateContext.mockReset();
mocks.syncBrowserWormholeGateState.mockResolvedValue(true);
mocks.getGateSessionStreamStatus.mockReturnValue({
enabled: false,
phase: 'idle',
transport: 'sse',
sessionId: '',
subscriptions: [],
heartbeatS: 0,
batchMs: 0,
lastEventType: '',
lastEventAt: 0,
detail: '',
});
mocks.retainGateSessionStreamGate.mockReturnValue(vi.fn());
mocks.subscribeGateSessionStreamEvents.mockReturnValue(vi.fn());
mocks.subscribeGateSessionStreamStatus.mockImplementation((listener: (status: unknown) => void) => {
streamStatusListeners.push(listener);
listener(mocks.getGateSessionStreamStatus());
return vi.fn();
});
mocks.fetchWormholeGateKeyStatus.mockResolvedValue({
ok: true,
has_local_access: true,
identity_scope: 'gate',
});
mocks.controlPlaneJson.mockResolvedValue({
node_id: '!sb_local',
proof: 'proof-token',
ts: 1712345678,
});
mocks.decryptWormholeGateMessages.mockResolvedValue({
ok: true,
results: [
{
ok: true,
gate_id: 'infonet',
epoch: 7,
plaintext: 'sealed',
identity_scope: 'browser_privacy_core',
},
],
});
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.includes('/api/mesh/infonet/messages')) {
return {
ok: true,
json: async () => ({
messages: [
{
event_id: 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/reputation/batch')) {
return {
ok: true,
json: async () => ({ reputations: {} }),
};
}
throw new Error(`unexpected fetch url: ${url}`);
}),
);
Object.defineProperty(Element.prototype, 'scrollIntoView', {
configurable: true,
value: vi.fn(),
});
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
const emitStreamStatus = (status: {
enabled: boolean;
phase: 'idle' | 'connecting' | 'open' | 'closed' | 'disabled' | 'error';
transport: 'sse';
sessionId: string;
subscriptions: string[];
heartbeatS: number;
batchMs: number;
lastEventType: string;
lastEventAt: number;
detail: string;
}) => {
mocks.getGateSessionStreamStatus.mockReturnValue(status);
streamStatusListeners.forEach((listener) => listener(status));
};
it('shows a clear room error when browser-local gate runtime is required', async () => {
mocks.decryptWormholeGateMessages.mockRejectedValue(
new Error('gate_local_runtime_required:browser_gate_state_resync_required:infonet'),
);
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(
await screen.findByText(
'Local infonet state needs a resync on this device. Use native desktop or resync local gate state.',
),
).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'ENABLE FOR ROOM' })).not.toBeInTheDocument();
expect(mocks.syncBrowserWormholeGateState).toHaveBeenCalledWith('infonet');
expect(mocks.decryptWormholeGateMessages).toHaveBeenCalledWith([
expect.objectContaining({
gate_id: 'infonet',
ciphertext: 'ciphertext-1',
envelope_hash: 'hash-1',
}),
]);
});
it('keeps recovery-only decrypt failures out of the red room-error path', async () => {
mocks.decryptWormholeGateMessages.mockResolvedValue({
ok: true,
results: [
{
ok: false,
detail: 'gate_backend_decrypt_recovery_only',
gate_id: 'infonet',
compat_requested: true,
compat_effective: false,
},
],
});
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
await waitFor(() => expect(mocks.decryptWormholeGateMessages).toHaveBeenCalled());
expect(screen.queryByText('COMPAT MODE')).not.toBeInTheDocument();
expect(
screen.queryByText(
'Service-side gate decrypt is disabled on this runtime. Use native desktop or an explicit recovery path.',
),
).not.toBeInTheDocument();
expect(screen.getByText('sealed')).toBeInTheDocument();
});
it('shows a friendly room message instead of a raw transport-tier gate post failure', async () => {
mocks.postWormholeGateMessage.mockRejectedValue(new Error('transport tier insufficient'));
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(await screen.findByText('sealed')).toBeInTheDocument();
await waitFor(() => {
expect(mocks.prepareWormholeInteractiveLane).toHaveBeenCalledWith({
minimumTransportTier: 'private_control_only',
});
});
fireEvent.change(screen.getByPlaceholderText('Post into this gate...'), {
target: { value: 'hello' },
});
fireEvent.click(screen.getByRole('button', { name: /post/i }));
expect(
await screen.findByText(
'The obfuscated lane is still warming up in the background. Stay in the room and posting should unlock shortly.',
),
).toBeInTheDocument();
});
it('shows a friendly room message instead of a raw gate-envelope post failure', async () => {
mocks.postWormholeGateMessage.mockRejectedValue(new Error('gate_envelope_required'));
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(await screen.findByText('sealed')).toBeInTheDocument();
await waitFor(() => {
expect(mocks.prepareWormholeInteractiveLane).toHaveBeenCalledWith({
minimumTransportTier: 'private_control_only',
});
});
fireEvent.change(screen.getByPlaceholderText('Post into this gate...'), {
target: { value: 'hello' },
});
fireEvent.click(screen.getByRole('button', { name: /post/i }));
expect(
await screen.findByText('Local gate sealing is warming up. Your draft is still here.'),
).toBeInTheDocument();
});
it('does one initial gate fetch and then switches to wait-for-change reads', async () => {
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.includes('/api/mesh/infonet/messages/wait?')) {
return {
ok: true,
json: async () => ({
gate: 'infonet',
changed: false,
cursor: 1,
messages: [
{
event_id: 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/infonet/messages?gate=')) {
return {
ok: true,
json: async () => ({
cursor: 1,
messages: [
{
event_id: 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/reputation/batch')) {
return {
ok: true,
json: async () => ({ reputations: {} }),
};
}
throw new Error(`unexpected fetch url: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const gateSnapshotModule = await import('@/mesh/gateMessageSnapshot');
const fetchSnapshotSpy = vi.spyOn(gateSnapshotModule, 'fetchGateMessageSnapshotState');
const waitSnapshotSpy = vi.spyOn(gateSnapshotModule, 'waitForGateMessageSnapshot');
gateSnapshotModule.invalidateGateMessageSnapshot('infonet');
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(await screen.findByText('sealed')).toBeInTheDocument();
await waitFor(() =>
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).includes('/api/mesh/infonet/messages/wait?gate=infonet&after=1'),
),
).toBe(true),
);
expect(fetchSnapshotSpy).toHaveBeenCalledWith('infonet', 40, expect.any(Object));
expect(waitSnapshotSpy).toHaveBeenCalledWith(
'infonet',
1,
40,
expect.objectContaining({ timeoutMs: expect.any(Number), signal: expect.any(Object) }),
);
});
it('uses stream-driven room updates as the steady-state path when the gate session stream is open', async () => {
const streamEventListeners: Array<(event: { event: string; data: unknown }) => void> = [];
mocks.getGateSessionStreamAccessHeaders.mockReturnValue({
'X-Wormhole-Node-Id': '!sb_stream',
'X-Wormhole-Gate-Proof': 'proof-stream',
'X-Wormhole-Gate-Ts': '1712360000',
});
emitStreamStatus({
enabled: true,
phase: 'open',
transport: 'sse',
sessionId: 'sess-1',
subscriptions: ['infonet'],
heartbeatS: 20,
batchMs: 1500,
lastEventType: 'hello',
lastEventAt: 1712360000,
detail: '',
});
mocks.subscribeGateSessionStreamStatus.mockImplementation((listener: (status: unknown) => void) => {
streamStatusListeners.push(listener);
listener(mocks.getGateSessionStreamStatus());
return vi.fn();
});
mocks.subscribeGateSessionStreamEvents.mockImplementation((listener: (event: { event: string; data: unknown }) => void) => {
streamEventListeners.push(listener);
return vi.fn();
});
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.includes('/api/mesh/infonet/messages?gate=')) {
return {
ok: true,
json: async () => ({
cursor: url.includes('force') ? 2 : 1,
messages: [
{
event_id: url.includes('force') ? 'evt-2' : 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/reputation/batch')) {
return {
ok: true,
json: async () => ({ reputations: {} }),
};
}
throw new Error(`unexpected fetch url: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const gateSnapshotModule = await import('@/mesh/gateMessageSnapshot');
const fetchSnapshotSpy = vi.spyOn(gateSnapshotModule, 'fetchGateMessageSnapshotState');
const waitSnapshotSpy = vi.spyOn(gateSnapshotModule, 'waitForGateMessageSnapshot');
gateSnapshotModule.invalidateGateMessageSnapshot('infonet');
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(await screen.findByText('sealed')).toBeInTheDocument();
expect(mocks.subscribeGateSessionStreamEvents).toHaveBeenCalled();
expect(mocks.fetchWormholeGateKeyStatus).toHaveBeenCalledWith(
'infonet',
expect.objectContaining({ mode: 'session_stream' }),
);
expect(mocks.controlPlaneJson).not.toHaveBeenCalled();
waitSnapshotSpy.mockClear();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(waitSnapshotSpy).not.toHaveBeenCalled();
streamEventListeners.forEach((listener) =>
listener({
event: 'gate_update',
data: {
session_id: 'sess-1',
updates: [{ gate_id: 'infonet', cursor: 2 }],
ts: 1712360001,
},
}),
);
await waitFor(() =>
expect(fetchSnapshotSpy).toHaveBeenCalledWith(
'infonet',
40,
expect.objectContaining({ force: true, proofMode: 'session_stream' }),
),
);
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).includes('/api/mesh/infonet/messages/wait?'),
),
).toBe(false);
expect(mocks.controlPlaneJson).not.toHaveBeenCalled();
});
it('falls back to wait-for-change on stream loss and hands control back after reconnect', async () => {
const streamEventListeners: Array<(event: { event: string; data: unknown }) => void> = [];
emitStreamStatus({
enabled: true,
phase: 'open',
transport: 'sse',
sessionId: 'sess-2',
subscriptions: ['infonet'],
heartbeatS: 20,
batchMs: 1500,
lastEventType: 'hello',
lastEventAt: 1712360100,
detail: '',
});
mocks.subscribeGateSessionStreamEvents.mockImplementation((listener: (event: { event: string; data: unknown }) => void) => {
streamEventListeners.push(listener);
return vi.fn();
});
const fetchMock = vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.includes('/api/mesh/infonet/messages/wait?')) {
return {
ok: true,
json: async () => ({
gate: 'infonet',
changed: false,
cursor: 1,
messages: [
{
event_id: 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/infonet/messages?gate=')) {
return {
ok: true,
json: async () => ({
cursor: 1,
messages: [
{
event_id: 'evt-1',
timestamp: 1712345678,
payload: {
gate: 'infonet',
ciphertext: 'ciphertext-1',
nonce: 'nonce-1',
sender_ref: 'sender-ref-1',
format: 'mls1',
gate_envelope: 'gate-envelope-1',
envelope_hash: 'hash-1',
},
},
],
}),
};
}
if (url.includes('/api/mesh/reputation/batch')) {
return {
ok: true,
json: async () => ({ reputations: {} }),
};
}
throw new Error(`unexpected fetch url: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const gateSnapshotModule = await import('@/mesh/gateMessageSnapshot');
const fetchSnapshotSpy = vi.spyOn(gateSnapshotModule, 'fetchGateMessageSnapshotState');
const waitSnapshotSpy = vi.spyOn(gateSnapshotModule, 'waitForGateMessageSnapshot');
gateSnapshotModule.invalidateGateMessageSnapshot('infonet');
const { default: GateView } = await import('@/components/InfonetTerminal/GateView');
render(
<GateView
gateName="infonet"
persona="!sb_local"
onBack={() => {}}
onNavigateGate={() => {}}
availableGates={['infonet']}
/>,
);
expect(await screen.findByText('sealed')).toBeInTheDocument();
waitSnapshotSpy.mockClear();
emitStreamStatus({
enabled: false,
phase: 'closed',
transport: 'sse',
sessionId: 'sess-2',
subscriptions: ['infonet'],
heartbeatS: 20,
batchMs: 1500,
lastEventType: 'heartbeat',
lastEventAt: 1712360200,
detail: 'gate_session_stream_closed',
});
await waitFor(() =>
expect(waitSnapshotSpy).toHaveBeenCalledWith(
'infonet',
1,
40,
expect.objectContaining({ timeoutMs: expect.any(Number), signal: expect.any(Object) }),
),
);
waitSnapshotSpy.mockClear();
fetchSnapshotSpy.mockClear();
emitStreamStatus({
enabled: true,
phase: 'open',
transport: 'sse',
sessionId: 'sess-3',
subscriptions: ['infonet'],
heartbeatS: 20,
batchMs: 1500,
lastEventType: 'hello',
lastEventAt: 1712360300,
detail: '',
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(waitSnapshotSpy).not.toHaveBeenCalled();
streamEventListeners.forEach((listener) =>
listener({
event: 'gate_update',
data: {
session_id: 'sess-3',
updates: [{ gate_id: 'infonet', cursor: 2 }],
ts: 1712360301,
},
}),
);
await waitFor(() =>
expect(fetchSnapshotSpy).toHaveBeenCalledWith(
'infonet',
40,
expect.objectContaining({ force: true }),
),
);
});
});
@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const getNodeIdentity = vi.fn(() => null);
const getWormholeIdentityDescriptor = vi.fn(() => ({ nodeId: '!sb_scope_a' }));
vi.mock('@/mesh/meshIdentity', () => ({
getNodeIdentity,
getWormholeIdentityDescriptor,
}));
describe('gateCompatTelemetry', () => {
beforeEach(() => {
vi.resetModules();
window.localStorage.clear();
window.sessionStorage.clear();
getNodeIdentity.mockReset();
getNodeIdentity.mockReturnValue(null);
getWormholeIdentityDescriptor.mockReset();
getWormholeIdentityDescriptor.mockReturnValue({ nodeId: '!sb_scope_a' });
});
it('records required and used compat events with reason summaries', async () => {
const mod = await import('@/mesh/gateCompatTelemetry');
mod.recordGateCompatTelemetry({
gateId: 'infonet',
action: 'decrypt',
reason: 'browser_gate_state_resync_required:infonet',
kind: 'required',
at: 1712500000000,
});
mod.recordGateCompatTelemetry({
gateId: 'infonet',
action: 'decrypt',
reason: 'browser_gate_state_resync_required:infonet',
kind: 'used',
at: 1712500005000,
});
const snapshot = mod.getGateCompatTelemetrySnapshot();
expect(snapshot.totalRequired).toBe(1);
expect(snapshot.totalUsed).toBe(1);
expect(snapshot.reasons[0]).toEqual(
expect.objectContaining({
reason: 'browser_gate_state_resync_required:infonet',
requiredCount: 1,
usedCount: 1,
recentGates: ['infonet'],
}),
);
});
it('keeps telemetry scoped to the current browser profile across reloads', async () => {
const mod = await import('@/mesh/gateCompatTelemetry');
mod.recordGateCompatTelemetry({
gateId: 'infonet',
action: 'compose',
reason: 'browser_gate_worker_unavailable',
kind: 'required',
at: 1712501000000,
});
vi.resetModules();
getWormholeIdentityDescriptor.mockReturnValue({ nodeId: '!sb_scope_a' });
const reloaded = await import('@/mesh/gateCompatTelemetry');
expect(reloaded.getGateCompatTelemetrySnapshot().totalRequired).toBe(1);
vi.resetModules();
getWormholeIdentityDescriptor.mockReturnValue({ nodeId: '!sb_scope_b' });
const otherScope = await import('@/mesh/gateCompatTelemetry');
expect(otherScope.getGateCompatTelemetrySnapshot().totalRequired).toBe(0);
});
});
@@ -62,7 +62,13 @@ describe('gate envelope display', () => {
expect(isEncryptedGateEnvelope(encrypted)).toBe(true);
expect(gateEnvelopeState(encrypted)).toBe('locked');
expect(gateEnvelopeDisplayText(encrypted)).toBe('ENCRYPTED GATE MESSAGE - KEY UNAVAILABLE');
expect(gateEnvelopeDisplayText(encrypted)).toBe('Sealed message - durable gate envelope was not stored.');
expect(
gateEnvelopeDisplayText({
...encrypted,
gate_envelope: 'opaque-envelope',
}),
).toBe('Sealed message - waiting for local gate decrypt.');
expect(
gateEnvelopeState({
...encrypted,
@@ -0,0 +1,250 @@
/**
* P5A: End-to-end gate envelope hash binding on the live decrypt path.
*
* Tests prove:
* - normalizeInfoNetMessage preserves envelope_hash from payload
* - normalizeInfoNetMessage preserves top-level envelope_hash
* - legacy messages without envelope_hash are not broken
* - WormholeGateDecryptPayload shape includes envelope_hash
* - decryptWormholeGateMessage single-message helper accepts integrity fields
* - MeshTerminal normalizer pattern preserves gate_envelope and envelope_hash
*/
import { describe, expect, it } from 'vitest';
import type { InfoNetMessage } from '@/components/MeshChat/types';
import { normalizeInfoNetMessage } from '@/components/MeshChat/utils';
import {
decryptWormholeGateMessage,
type WormholeGateDecryptPayload,
} from '@/mesh/wormholeIdentityClient';
describe('normalizeInfoNetMessage preserves envelope_hash', () => {
it('extracts envelope_hash from nested payload', () => {
const raw: InfoNetMessage = {
event_id: 'e1',
timestamp: 1000,
payload: {
gate: 'finance',
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
format: 'mls1',
envelope_hash: 'abc123hash',
},
};
const normalized = normalizeInfoNetMessage(raw);
expect(normalized.envelope_hash).toBe('abc123hash');
});
it('preserves top-level envelope_hash over payload', () => {
const raw: InfoNetMessage = {
event_id: 'e2',
timestamp: 2000,
envelope_hash: 'top-level-hash',
payload: {
gate: 'finance',
ciphertext: 'ct',
nonce: 'n2',
sender_ref: 'sr2',
format: 'mls1',
envelope_hash: 'payload-hash',
},
};
const normalized = normalizeInfoNetMessage(raw);
expect(normalized.envelope_hash).toBe('top-level-hash');
});
it('returns empty string when no envelope_hash present', () => {
const raw: InfoNetMessage = {
event_id: 'e3',
timestamp: 3000,
payload: {
gate: 'finance',
ciphertext: 'ct',
nonce: 'n3',
sender_ref: 'sr3',
format: 'mls1',
},
};
const normalized = normalizeInfoNetMessage(raw);
expect(normalized.envelope_hash).toBe('');
});
it('does not break messages without payload', () => {
const raw: InfoNetMessage = {
event_id: 'e4',
timestamp: 4000,
ciphertext: 'ct',
gate_envelope: 'env',
envelope_hash: 'hash4',
};
const normalized = normalizeInfoNetMessage(raw);
// No payload → returns message as-is, envelope_hash untouched
expect(normalized.envelope_hash).toBe('hash4');
});
});
describe('WormholeGateDecryptPayload supports envelope_hash', () => {
it('accepts envelope_hash in the payload type', () => {
const payload: WormholeGateDecryptPayload = {
gate_id: 'gate1',
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
format: 'mls1',
gate_envelope: 'env',
envelope_hash: 'abc123hash',
};
expect(payload.envelope_hash).toBe('abc123hash');
});
it('allows omitting envelope_hash for legacy compatibility', () => {
const payload: WormholeGateDecryptPayload = {
gate_id: 'gate1',
ciphertext: 'ct',
};
expect(payload.envelope_hash).toBeUndefined();
});
});
describe('decrypt caller payload construction includes envelope_hash', () => {
it('builds decrypt payload with envelope_hash when present on message', () => {
// Simulates the payload construction pattern used in GateView and useMeshChatController
const message = {
gate: 'finance',
epoch: 2,
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
format: 'mls1',
gate_envelope: 'env-data',
envelope_hash: 'sha256-hex-hash',
};
const decryptPayload: WormholeGateDecryptPayload = {
gate_id: String(message.gate || ''),
epoch: Number(message.epoch || 0),
ciphertext: String(message.ciphertext || ''),
nonce: String(message.nonce || ''),
sender_ref: String(message.sender_ref || ''),
format: String(message.format || 'mls1'),
gate_envelope: String(message.gate_envelope || ''),
envelope_hash: String(message.envelope_hash || ''),
};
expect(decryptPayload.envelope_hash).toBe('sha256-hex-hash');
expect(decryptPayload.gate_envelope).toBe('env-data');
});
it('builds decrypt payload with empty envelope_hash for legacy messages', () => {
const message = {
gate: 'finance',
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
format: 'mls1',
gate_envelope: 'env-data',
};
const decryptPayload: WormholeGateDecryptPayload = {
gate_id: String(message.gate || ''),
epoch: 0,
ciphertext: String(message.ciphertext || ''),
nonce: String(message.nonce || ''),
sender_ref: String(message.sender_ref || ''),
format: String(message.format || 'mls1'),
gate_envelope: String(message.gate_envelope || ''),
envelope_hash: String((message as Record<string, unknown>).envelope_hash || ''),
};
expect(decryptPayload.envelope_hash).toBe('');
});
});
describe('single-message decryptWormholeGateMessage accepts integrity fields', () => {
it('function signature accepts gate_envelope and envelope_hash', async () => {
// Verify the function exists and accepts the extended signature.
// We cannot call it without a running backend, but we can verify
// the function shape by checking it is callable with 7 args.
expect(typeof decryptWormholeGateMessage).toBe('function');
expect(decryptWormholeGateMessage.length).toBeLessThanOrEqual(8);
});
});
describe('MeshTerminal normalizeInfonetMessageRecord equivalent pattern', () => {
it('preserves gate_envelope and envelope_hash from nested payload', () => {
// Simulate the normalizeInfonetMessageRecord pattern from MeshTerminal
const message: Record<string, unknown> = {
event_id: 'e1',
timestamp: 1000,
payload: {
gate: 'finance',
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
format: 'mls1',
gate_envelope: 'env-payload',
envelope_hash: 'hash-payload',
},
};
const payload = message.payload as Record<string, string> | undefined;
const normalized = {
...message,
gate: String(message.gate ?? payload?.gate ?? ''),
ciphertext: String(message.ciphertext ?? payload?.ciphertext ?? ''),
nonce: String(message.nonce ?? payload?.nonce ?? ''),
sender_ref: String(message.sender_ref ?? payload?.sender_ref ?? ''),
format: String(message.format ?? payload?.format ?? ''),
gate_envelope: String(message.gate_envelope ?? payload?.gate_envelope ?? ''),
envelope_hash: String(message.envelope_hash ?? payload?.envelope_hash ?? ''),
};
expect(normalized.gate_envelope).toBe('env-payload');
expect(normalized.envelope_hash).toBe('hash-payload');
});
it('single decrypt call site passes integrity fields through', () => {
const normalized = {
gate: 'finance',
epoch: 2,
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
gate_envelope: 'env-data',
envelope_hash: 'sha256-hex',
};
// Matches the call pattern in describeGateMessage
const args = [
String(normalized.gate || ''),
Number(normalized.epoch || 0),
String(normalized.ciphertext || ''),
String(normalized.nonce || ''),
String(normalized.sender_ref || ''),
String(normalized.gate_envelope || ''),
String(normalized.envelope_hash || ''),
];
expect(args[5]).toBe('env-data');
expect(args[6]).toBe('sha256-hex');
});
it('legacy message without integrity fields produces empty strings', () => {
const normalized = {
gate: 'finance',
epoch: 1,
ciphertext: 'ct',
nonce: 'n1',
sender_ref: 'sr1',
};
const args = [
String(normalized.gate || ''),
Number(normalized.epoch || 0),
String(normalized.ciphertext || ''),
String(normalized.nonce || ''),
String(normalized.sender_ref || ''),
String((normalized as any).gate_envelope || ''),
String((normalized as any).envelope_hash || ''),
];
expect(args[5]).toBe('');
expect(args[6]).toBe('');
});
});
@@ -0,0 +1,342 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hasLocalControlBridge = vi.fn(() => false);
const buildGateAccessHeaders = vi.fn();
vi.mock('@/lib/localControlTransport', () => ({
hasLocalControlBridge,
}));
vi.mock('@/mesh/gateAccessProof', () => ({
buildGateAccessHeaders,
}));
describe('gateMessageSnapshot cache', () => {
const fetchMock = vi.fn();
beforeEach(() => {
vi.resetModules();
fetchMock.mockReset();
buildGateAccessHeaders.mockReset();
hasLocalControlBridge.mockReset();
hasLocalControlBridge.mockReturnValue(false);
buildGateAccessHeaders.mockResolvedValue({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof',
'X-Wormhole-Gate-Ts': '1712345678',
});
vi.stubGlobal('fetch', fetchMock);
});
it('coarsens browser/web gate message reads through a short shared cache window', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:45:00.000Z'));
try {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-1', gate: 'infonet', timestamp: 1712360000 }],
}),
});
const mod = await import('@/mesh/gateMessageSnapshot');
await expect(mod.fetchGateMessageSnapshot('infonet', 20)).resolves.toEqual([
expect.objectContaining({ event_id: 'evt-1', gate: 'infonet' }),
]);
await mod.fetchGateMessageSnapshot('infonet', 20);
expect(fetchMock).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(10_001);
await mod.fetchGateMessageSnapshot('infonet', 20);
expect(fetchMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('reuses a larger cached limit for smaller reads without another fetch', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
messages: Array.from({ length: 8 }, (_, index) => ({
event_id: `evt-${index + 1}`,
gate: 'finance',
timestamp: 1712360000 + index,
})),
}),
});
const mod = await import('@/mesh/gateMessageSnapshot');
await expect(mod.fetchGateMessageSnapshot('finance', 8)).resolves.toHaveLength(8);
await expect(mod.fetchGateMessageSnapshot('finance', 4)).resolves.toHaveLength(4);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('uses session-stream proof reuse for stream-owned snapshot refreshes', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-1', gate: 'finance', timestamp: 1712360000 }],
cursor: 1,
}),
});
const mod = await import('@/mesh/gateMessageSnapshot');
await expect(
mod.fetchGateMessageSnapshotState('finance', 20, { proofMode: 'session_stream' }),
).resolves.toEqual({
messages: [expect.objectContaining({ event_id: 'evt-1', gate: 'finance' })],
cursor: 1,
});
expect(buildGateAccessHeaders).toHaveBeenCalledWith('finance', { mode: 'session_stream' });
});
it('reuses a larger in-flight snapshot fetch for a smaller concurrent read', async () => {
let releaseFetch:
| ((value: {
ok: true;
json: () => Promise<{
messages: Array<{ event_id: string; gate: string; timestamp: number }>;
cursor: number;
}>;
}) => void)
| null = null;
fetchMock.mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseFetch = resolve as typeof releaseFetch;
}),
);
const mod = await import('@/mesh/gateMessageSnapshot');
const larger = mod.fetchGateMessageSnapshotState('infonet', 40);
const smaller = mod.fetchGateMessageSnapshotState('infonet', 20);
await Promise.resolve();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/api/mesh/infonet/messages?gate=infonet&limit=40');
releaseFetch?.({
ok: true,
json: async () => ({
messages: Array.from({ length: 3 }, (_, index) => ({
event_id: `evt-${index + 1}`,
gate: 'infonet',
timestamp: 1712360000 + index,
})),
cursor: 3,
}),
});
await expect(larger).resolves.toEqual({
messages: [
expect.objectContaining({ event_id: 'evt-1', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-2', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-3', gate: 'infonet' }),
],
cursor: 3,
});
await expect(smaller).resolves.toEqual({
messages: [
expect.objectContaining({ event_id: 'evt-1', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-2', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-3', gate: 'infonet' }),
],
cursor: 3,
});
});
it('uses a shorter native cache window and supports explicit invalidation', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:45:00.000Z'));
try {
hasLocalControlBridge.mockReturnValue(true);
fetchMock
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-1', gate: 'ops', timestamp: 1712360000 }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-2', gate: 'ops', timestamp: 1712360010 }],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-3', gate: 'ops', timestamp: 1712360020 }],
}),
});
const mod = await import('@/mesh/gateMessageSnapshot');
await expect(mod.fetchGateMessageSnapshot('ops', 6)).resolves.toEqual([
expect.objectContaining({ event_id: 'evt-1' }),
]);
vi.advanceTimersByTime(3_001);
await expect(mod.fetchGateMessageSnapshot('ops', 6)).resolves.toEqual([
expect.objectContaining({ event_id: 'evt-2' }),
]);
mod.invalidateGateMessageSnapshot('ops');
await expect(mod.fetchGateMessageSnapshot('ops', 6)).resolves.toEqual([
expect.objectContaining({ event_id: 'evt-3' }),
]);
expect(fetchMock).toHaveBeenCalledTimes(3);
} finally {
vi.useRealTimers();
}
});
it('tracks cursors and waits for gate changes without re-reading the ordinary route', async () => {
fetchMock
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-1', gate: 'infonet', timestamp: 1712360000 }],
cursor: 1,
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [
{ event_id: 'evt-2', gate: 'infonet', timestamp: 1712360010 },
{ event_id: 'evt-1', gate: 'infonet', timestamp: 1712360000 },
],
cursor: 2,
changed: true,
}),
});
const mod = await import('@/mesh/gateMessageSnapshot');
await expect(mod.fetchGateMessageSnapshotState('infonet', 20)).resolves.toEqual({
messages: [expect.objectContaining({ event_id: 'evt-1', gate: 'infonet' })],
cursor: 1,
});
await expect(mod.waitForGateMessageSnapshot('infonet', 1, 20, { timeoutMs: 18_000 })).resolves.toEqual({
messages: [
expect.objectContaining({ event_id: 'evt-2', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-1', gate: 'infonet' }),
],
cursor: 2,
changed: true,
});
expect(mod.getGateMessageSnapshotCursor('infonet')).toBe(2);
expect(fetchMock.mock.calls[1]?.[0]).toContain('/api/mesh/infonet/messages/wait?gate=infonet&after=1');
});
it('coalesces concurrent gate wait requests for the same gate cursor', async () => {
let releaseWait:
| ((value: { ok: true; json: () => Promise<{ messages: Array<{ event_id: string; gate: string; timestamp: number }>; cursor: number; changed: boolean }> }) => void)
| null = null;
fetchMock.mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseWait = resolve as typeof releaseWait;
}),
);
const mod = await import('@/mesh/gateMessageSnapshot');
const first = mod.waitForGateMessageSnapshot('infonet', 4, 20, { timeoutMs: 18_000 });
const second = mod.waitForGateMessageSnapshot('infonet', 4, 20, { timeoutMs: 24_000 });
await Promise.resolve();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/api/mesh/infonet/messages/wait?gate=infonet&after=4');
releaseWait?.({
ok: true,
json: async () => ({
messages: [{ event_id: 'evt-5', gate: 'infonet', timestamp: 1712360050 }],
cursor: 5,
changed: true,
}),
});
await expect(first).resolves.toEqual({
messages: [expect.objectContaining({ event_id: 'evt-5', gate: 'infonet' })],
cursor: 5,
changed: true,
});
await expect(second).resolves.toEqual({
messages: [expect.objectContaining({ event_id: 'evt-5', gate: 'infonet' })],
cursor: 5,
changed: true,
});
});
it('reuses a larger in-flight gate wait for a smaller concurrent consumer', async () => {
let releaseWait:
| ((value: {
ok: true;
json: () => Promise<{
messages: Array<{ event_id: string; gate: string; timestamp: number }>;
cursor: number;
changed: boolean;
}>;
}) => void)
| null = null;
fetchMock.mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseWait = resolve as typeof releaseWait;
}),
);
const mod = await import('@/mesh/gateMessageSnapshot');
const larger = mod.waitForGateMessageSnapshot('infonet', 4, 40, { timeoutMs: 18_000 });
const smaller = mod.waitForGateMessageSnapshot('infonet', 4, 20, { timeoutMs: 24_000 });
await Promise.resolve();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/api/mesh/infonet/messages/wait?gate=infonet&after=4&limit=40');
releaseWait?.({
ok: true,
json: async () => ({
messages: [
{ event_id: 'evt-8', gate: 'infonet', timestamp: 1712360080 },
{ event_id: 'evt-7', gate: 'infonet', timestamp: 1712360070 },
],
cursor: 8,
changed: true,
}),
});
await expect(larger).resolves.toEqual({
messages: [
expect.objectContaining({ event_id: 'evt-8', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-7', gate: 'infonet' }),
],
cursor: 8,
changed: true,
});
await expect(smaller).resolves.toEqual({
messages: [
expect.objectContaining({ event_id: 'evt-8', gate: 'infonet' }),
expect.objectContaining({ event_id: 'evt-7', gate: 'infonet' }),
],
cursor: 8,
changed: true,
});
});
});
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hasLocalControlBridge = vi.fn(() => false);
vi.mock('@/lib/localControlTransport', () => ({
hasLocalControlBridge,
}));
describe('gate metadata timing policy', () => {
beforeEach(() => {
vi.resetModules();
hasLocalControlBridge.mockReset();
});
it('jittered browser/web polling avoids an exact cadence', async () => {
hasLocalControlBridge.mockReturnValue(false);
const mod = await import('@/mesh/gateMetadataTiming');
const pollDelays = Array.from({ length: 12 }, () => mod.nextGateMessagesPollDelayMs());
expect(pollDelays.every((delay) => delay >= 24_000 && delay <= 36_000)).toBe(true);
expect(new Set(pollDelays).size).toBeGreaterThan(1);
const waitTimeouts = Array.from({ length: 12 }, () => mod.nextGateMessagesWaitTimeoutMs());
expect(waitTimeouts.every((delay) => delay >= 26_000 && delay <= 38_000)).toBe(true);
expect(new Set(waitTimeouts).size).toBeGreaterThan(1);
const rearmDelays = Array.from({ length: 12 }, () => mod.nextGateMessagesWaitRearmDelayMs());
expect(rearmDelays.every((delay) => delay >= 3_000 && delay <= 4_200)).toBe(true);
expect(new Set(rearmDelays).size).toBeGreaterThan(1);
const refreshDelays = Array.from({ length: 12 }, () => mod.nextGateActivityRefreshDelayMs());
expect(refreshDelays.every((delay) => delay >= 4_500 && delay <= 9_500)).toBe(true);
expect(new Set(refreshDelays).size).toBeGreaterThan(1);
});
it('native desktop keeps the tighter poll/send timing path', async () => {
hasLocalControlBridge.mockReturnValue(true);
const mod = await import('@/mesh/gateMetadataTiming');
expect(mod.shouldJitterGateMetadataTiming()).toBe(false);
expect(mod.nextGateMessagesPollDelayMs()).toBe(30_000);
expect(mod.nextGateMessagesWaitTimeoutMs()).toBe(20_000);
expect(mod.nextGateMessagesWaitRearmDelayMs()).toBe(750);
expect(mod.nextGateActivityRefreshDelayMs()).toBe(0);
});
it('coarsens hidden browser tab gate polling further', async () => {
hasLocalControlBridge.mockReturnValue(false);
const originalVisibility = Object.getOwnPropertyDescriptor(document, 'visibilityState');
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'hidden',
});
try {
const mod = await import('@/mesh/gateMetadataTiming');
const pollDelays = Array.from({ length: 12 }, () => mod.nextGateMessagesPollDelayMs());
expect(pollDelays.every((delay) => delay >= 48_000 && delay <= 72_000)).toBe(true);
const waitTimeouts = Array.from({ length: 12 }, () => mod.nextGateMessagesWaitTimeoutMs());
expect(waitTimeouts.every((delay) => delay >= 60_000 && delay <= 84_000)).toBe(true);
const rearmDelays = Array.from({ length: 12 }, () => mod.nextGateMessagesWaitRearmDelayMs());
expect(rearmDelays.every((delay) => delay >= 6_000 && delay <= 12_000)).toBe(true);
const refreshDelays = Array.from({ length: 12 }, () => mod.nextGateActivityRefreshDelayMs());
expect(refreshDelays.every((delay) => delay >= 14_000 && delay <= 22_000)).toBe(true);
} finally {
if (originalVisibility) {
Object.defineProperty(document, 'visibilityState', originalVisibility);
} else {
delete (document as Document & { visibilityState?: string }).visibilityState;
}
}
});
});
@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hasLocalControlBridge = vi.fn(() => false);
const buildGateAccessHeaders = vi.fn();
const decryptWormholeGateMessage = vi.fn();
vi.mock('@/lib/localControlTransport', () => ({
hasLocalControlBridge,
}));
vi.mock('@/mesh/gateAccessProof', () => ({
buildGateAccessHeaders,
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
decryptWormholeGateMessage,
}));
describe('gatePreviewSnapshot cache', () => {
const fetchMock = vi.fn();
beforeEach(() => {
vi.resetModules();
fetchMock.mockReset();
buildGateAccessHeaders.mockReset();
decryptWormholeGateMessage.mockReset();
hasLocalControlBridge.mockReset();
hasLocalControlBridge.mockReturnValue(false);
buildGateAccessHeaders.mockResolvedValue({
'X-Wormhole-Node-Id': '!sb_gate',
'X-Wormhole-Gate-Proof': 'proof',
'X-Wormhole-Gate-Ts': '1712345678',
});
decryptWormholeGateMessage.mockResolvedValue({
ok: true,
plaintext: 'sealed preview',
});
vi.stubGlobal('fetch', fetchMock);
});
it('coarsens browser/web gate preview fetches through a short cache window', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:30:00.000Z'));
try {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
messages: [
{
event_id: 'evt-1',
event_type: 'gate_message',
node_id: '!sb_sender',
gate: 'infonet',
epoch: 7,
ciphertext: 'ct',
nonce: 'nonce',
sender_ref: 'sender-ref',
format: 'mls1',
gate_envelope: 'env',
envelope_hash: 'hash',
timestamp: Math.floor(Date.now() / 1000) - 60,
},
],
}),
});
const mod = await import('@/mesh/gatePreviewSnapshot');
await expect(mod.fetchGateThreadPreviewSnapshot('infonet')).resolves.toEqual([
{
nodeId: '!sb_sender',
age: '1m ago',
text: 'sealed preview',
encrypted: true,
},
]);
await mod.fetchGateThreadPreviewSnapshot('infonet');
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(decryptWormholeGateMessage).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(12_001);
await mod.fetchGateThreadPreviewSnapshot('infonet');
expect(fetchMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('uses a shorter preview cache window on native runtimes', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-05T23:30:00.000Z'));
try {
hasLocalControlBridge.mockReturnValue(true);
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
messages: [
{
event_id: 'evt-1',
node_id: '!sb_sender',
message: 'plain preview',
timestamp: Math.floor(Date.now() / 1000) - 60,
},
],
}),
});
const mod = await import('@/mesh/gatePreviewSnapshot');
await mod.fetchGateThreadPreviewSnapshot('infonet');
vi.advanceTimersByTime(4_001);
await mod.fetchGateThreadPreviewSnapshot('infonet');
expect(fetchMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('invalidates cached gate previews explicitly', async () => {
fetchMock
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [
{
event_id: 'evt-1',
node_id: '!sb_sender',
message: 'plain preview',
timestamp: 1712360000,
},
],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
messages: [
{
event_id: 'evt-2',
node_id: '!sb_sender',
message: 'updated preview',
timestamp: 1712360100,
},
],
}),
});
const mod = await import('@/mesh/gatePreviewSnapshot');
await expect(mod.fetchGateThreadPreviewSnapshot('infonet')).resolves.toEqual([
expect.objectContaining({ text: 'plain preview' }),
]);
mod.invalidateGateThreadPreviewSnapshot('infonet');
await expect(mod.fetchGateThreadPreviewSnapshot('infonet')).resolves.toEqual([
expect.objectContaining({ text: 'updated preview' }),
]);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,292 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneFetch = vi.fn();
vi.mock('@/lib/controlPlane', () => ({
controlPlaneFetch,
}));
describe('gateSessionStream manager', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneFetch.mockReset();
});
it('marks the stream disabled when the backend feature flag is off', async () => {
controlPlaneFetch.mockResolvedValue(
new Response(JSON.stringify({ ok: false, detail: 'gate_session_stream_disabled' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
}),
);
const mod = await import('@/mesh/gateSessionStream');
mod.connectGateSessionStream();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mod.getGateSessionStreamStatus()).toMatchObject({
enabled: false,
phase: 'disabled',
detail: 'gate_session_stream_disabled',
});
});
it('parses hello and heartbeat events from the session stream skeleton', async () => {
const encoder = new TextEncoder();
controlPlaneFetch.mockResolvedValue(
new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
[
'event: hello',
'data: {"session_id":"sess-1","subscriptions":["alpha","beta"],"heartbeat_s":20,"batch_ms":1500,"transport":"sse","gate_access":{"alpha":{"node_id":"!node_alpha","proof":"proof-alpha","ts":"1712360000"}},"gate_key_status":{"alpha":{"ok":true,"gate_id":"alpha","current_epoch":7,"has_local_access":true}}}',
'',
'event: heartbeat',
'data: {"session_id":"sess-1","ts":1712360000}',
'',
].join('\n'),
),
);
controller.close();
},
}),
{
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
},
),
);
const mod = await import('@/mesh/gateSessionStream');
mod.setGateSessionStreamSubscriptions(['Alpha', 'beta']);
mod.connectGateSessionStream();
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
expect(controlPlaneFetch).toHaveBeenCalledWith(
'/api/mesh/infonet/session-stream?gates=alpha%2Cbeta',
expect.objectContaining({
requireAdminSession: true,
cache: 'no-store',
headers: { Accept: 'text/event-stream' },
}),
);
expect(mod.getGateSessionStreamStatus()).toMatchObject({
enabled: false,
phase: 'closed',
sessionId: 'sess-1',
subscriptions: ['alpha', 'beta'],
heartbeatS: 20,
batchMs: 1500,
lastEventType: 'heartbeat',
});
expect(mod.getGateSessionStreamAccessHeaders('alpha')).toEqual({
'X-Wormhole-Node-Id': '!node_alpha',
'X-Wormhole-Gate-Proof': 'proof-alpha',
'X-Wormhole-Gate-Ts': '1712360000',
});
expect(mod.getGateSessionStreamKeyStatus('alpha')).toEqual({
ok: true,
gate_id: 'alpha',
current_epoch: 7,
has_local_access: true,
});
});
it('retains one shared subscription set across multiple same-gate consumers', async () => {
controlPlaneFetch.mockResolvedValue(
new Response(JSON.stringify({ ok: false, detail: 'gate_session_stream_disabled' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
}),
);
const mod = await import('@/mesh/gateSessionStream');
const releaseA = mod.retainGateSessionStreamGate('Alpha');
const releaseB = mod.retainGateSessionStreamGate('alpha');
await new Promise((resolve) => setTimeout(resolve, 0));
expect(controlPlaneFetch).toHaveBeenCalledTimes(1);
expect(mod.getGateSessionStreamStatus().subscriptions).toEqual(['alpha']);
releaseA();
expect(mod.getGateSessionStreamStatus().subscriptions).toEqual(['alpha']);
releaseB();
expect(mod.getGateSessionStreamStatus()).toMatchObject({
phase: 'idle',
subscriptions: [],
});
});
it('can invalidate cached per-gate stream bootstrap context without dropping the stream status', async () => {
const encoder = new TextEncoder();
controlPlaneFetch.mockResolvedValue(
new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
[
'event: hello',
'data: {"session_id":"sess-ctx","subscriptions":["alpha"],"heartbeat_s":20,"batch_ms":1500,"transport":"sse","gate_access":{"alpha":{"node_id":"!node_alpha","proof":"proof-alpha","ts":"1712360000"}},"gate_key_status":{"alpha":{"ok":true,"gate_id":"alpha","current_epoch":7,"has_local_access":true}}}',
'',
].join('\n'),
),
);
controller.close();
},
}),
{
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
},
),
);
const mod = await import('@/mesh/gateSessionStream');
mod.retainGateSessionStreamGate('alpha');
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mod.getGateSessionStreamAccessHeaders('alpha')).toBeDefined();
expect(mod.getGateSessionStreamKeyStatus('alpha')).toBeTruthy();
mod.invalidateGateSessionStreamGateContext('alpha');
expect(mod.getGateSessionStreamAccessHeaders('alpha')).toBeUndefined();
expect(mod.getGateSessionStreamKeyStatus('alpha')).toBeNull();
expect(mod.getGateSessionStreamStatus().sessionId).toBe('sess-ctx');
});
it('emits parsed gate_update events to stream event listeners', async () => {
const encoder = new TextEncoder();
controlPlaneFetch.mockResolvedValue(
new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
[
'event: hello',
'data: {"session_id":"sess-2","subscriptions":["alpha"],"heartbeat_s":20,"batch_ms":1500,"transport":"sse"}',
'',
'event: gate_update',
'data: {"session_id":"sess-2","updates":[{"gate_id":"alpha","cursor":3}],"ts":1712360001}',
'',
].join('\n'),
),
);
controller.close();
},
}),
{
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
},
),
);
const mod = await import('@/mesh/gateSessionStream');
const events: Array<{ event: string; data: unknown }> = [];
const unsubscribe = mod.subscribeGateSessionStreamEvents((event) => {
events.push({ event: event.event, data: event.data });
});
mod.retainGateSessionStreamGate('alpha');
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
unsubscribe();
expect(events.some((event) => event.event === 'hello')).toBe(true);
expect(events).toContainEqual({
event: 'gate_update',
data: {
session_id: 'sess-2',
updates: [{ gate_id: 'alpha', cursor: 3 }],
ts: 1712360001,
},
});
});
it('reconnects with retained subscriptions after the stream closes', async () => {
vi.useFakeTimers();
try {
const encoder = new TextEncoder();
let callCount = 0;
controlPlaneFetch.mockImplementation(async () => {
callCount += 1;
if (callCount === 1) {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
[
'event: hello',
'data: {"session_id":"sess-3","subscriptions":["alpha"],"heartbeat_s":20,"batch_ms":1500,"transport":"sse"}',
'',
].join('\n'),
),
);
controller.close();
},
}),
{
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
},
);
}
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(
[
'event: hello',
'data: {"session_id":"sess-4","subscriptions":["alpha"],"heartbeat_s":20,"batch_ms":1500,"transport":"sse"}',
'',
].join('\n'),
),
);
},
}),
{
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
},
);
});
const mod = await import('@/mesh/gateSessionStream');
const release = mod.retainGateSessionStreamGate('alpha');
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(1_000);
await Promise.resolve();
await Promise.resolve();
expect(controlPlaneFetch).toHaveBeenCalledTimes(2);
expect(mod.getGateSessionStreamStatus()).toMatchObject({
enabled: true,
subscriptions: ['alpha'],
});
expect(['connecting', 'open']).toContain(mod.getGateSessionStreamStatus().phase);
release();
mod.disconnectGateSessionStream();
} finally {
vi.useRealTimers();
}
});
});
@@ -155,4 +155,14 @@ describe('mailbox claim privacy padding', () => {
expect(decoyTokens).toEqual(['decoy-0', 'decoy-1']);
expect(decoyTokens.every((token) => !realSharedTokens.includes(token))).toBe(true);
});
it('can build mailbox claims from a prepared Wormhole identity override', async () => {
deadDropTokensForContacts.mockResolvedValue([]);
const mod = await import('@/mesh/meshDmClient');
await mod.buildMailboxClaims({}, { nodeId: '!sb_wormhole_dm' });
expect(mailboxClaimToken).toHaveBeenCalledWith('self', '!sb_wormhole_dm');
expect(mailboxClaimToken).toHaveBeenCalledWith('requests', '!sb_wormhole_dm');
});
});
@@ -0,0 +1,141 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { isDmPollBlocked, isGateSendBlocked, shouldQueueDmSend } from '@/lib/meshChatPolicies';
function readSource(relativePath: string): string {
const here = path.dirname(fileURLToPath(import.meta.url));
return fs.readFileSync(path.resolve(here, relativePath), 'utf-8');
}
describe('MeshChat behavior - shouldQueueDmSend', () => {
it('returns false for default privacy profile', () => {
expect(shouldQueueDmSend('default')).toBe(false);
});
it('returns true for high privacy profile', () => {
expect(shouldQueueDmSend('high')).toBe(true);
});
});
describe('MeshChat behavior - isGateSendBlocked', () => {
it('blocks when on infonet tab with gate selected but access not ready', () => {
expect(isGateSendBlocked('infonet', true, false)).toBe(true);
});
it('does not block when gate access is ready', () => {
expect(isGateSendBlocked('infonet', true, true)).toBe(false);
});
it('does not block when no gate is selected', () => {
expect(isGateSendBlocked('infonet', false, false)).toBe(false);
});
it('does not block on non-infonet tabs', () => {
expect(isGateSendBlocked('dms', true, false)).toBe(false);
expect(isGateSendBlocked('meshtastic', true, false)).toBe(false);
expect(isGateSendBlocked('mesh', true, false)).toBe(false);
});
it('does not block when all conditions are false', () => {
expect(isGateSendBlocked('dms', false, true)).toBe(false);
});
});
describe('MeshChat behavior - isDmPollBlocked', () => {
it('blocks when wormhole is enabled but not ready', () => {
expect(isDmPollBlocked(true, false, false)).toBe(true);
});
it('blocks when anonymous DM is blocked', () => {
expect(isDmPollBlocked(false, false, true)).toBe(true);
});
it('blocks when both wormhole not ready and anonymous blocked', () => {
expect(isDmPollBlocked(true, false, true)).toBe(true);
});
it('does not block when wormhole is ready and anonymous is not blocked', () => {
expect(isDmPollBlocked(true, true, false)).toBe(false);
});
it('does not block when wormhole is disabled and anonymous is not blocked', () => {
expect(isDmPollBlocked(false, false, false)).toBe(false);
});
it('does not block when wormhole is disabled and ready', () => {
expect(isDmPollBlocked(false, true, false)).toBe(false);
});
});
describe('MeshChat behavior - policy wiring', () => {
it('controller imports all three policy functions from meshChatPolicies', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toMatch(
/import\s*\{[^}]*shouldQueueDmSend[^}]*\}\s*from\s+['"]@\/lib\/meshChatPolicies['"]/,
);
expect(controller).toMatch(
/import\s*\{[^}]*isGateSendBlocked[^}]*\}\s*from\s+['"]@\/lib\/meshChatPolicies['"]/,
);
expect(controller).toMatch(
/import\s*\{[^}]*isDmPollBlocked[^}]*\}\s*from\s+['"]@\/lib\/meshChatPolicies['"]/,
);
});
it('controller calls shouldQueueDmSend in enqueueDmSend', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain('shouldQueueDmSend(privacyProfile)');
});
it('controller calls isGateSendBlocked in handleSend', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain('isGateSendBlocked(');
});
it('controller calls isDmPollBlocked in DM poll effects', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain(
'isDmPollBlocked(wormholeEnabled, wormholeReadyState, anonymousDmBlocked)',
);
});
it('controller suppresses unread-count polling while the DMS tab owns mailbox refresh', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain("if (!hasId || !getDMNotify() || (expanded && activeTab === 'dms')) return;");
expect(controller).toContain("jitteredPollDelay(baseDelay, { profile: privacyProfile })");
});
it('controller uses the shared DM poll scheduler for live mailbox refresh cadence', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain('classifyTick(hasMore, catchUpBudget, DM_MESSAGES_POLL_MS');
expect(controller).toContain('timer = setTimeout(() => void poll(classification.refreshCount), classification.delay);');
});
it('dead-drop UI distinguishes invite-pinned trust from TOFU-only', () => {
const index = readSource('../../components/MeshChat/index.tsx');
expect(index).toContain('getContactTrustSummary');
expect(index).toContain('INVITE PINNED');
expect(index).toContain('TOFU ONLY');
expect(index).toContain('anchored by an imported signed invite');
expect(index).toContain('rootWitnessContinuityLabel');
expect(index).toContain('RECOVER ROOT');
expect(index).toContain('!selectedContactTrustSummary?.rootMismatch');
});
it('request UI does not route ordinary request flow through legacy add-contact lookup', () => {
const index = readSource('../../components/MeshChat/index.tsx');
expect(index).toContain('handleRequestComposerAction');
expect(index).not.toContain('handleAddContact().catch(() =>');
expect(index).toContain('dm add');
expect(index).toContain('legacy migration');
});
it('controller blocks trust-new-key when the stable root changed', () => {
const controller = readSource('../../components/MeshChat/useMeshChatController.ts');
expect(controller).toContain('contactInfo?.remotePrekeyRootMismatch');
expect(controller).toContain('stable root changed; use RECOVER ROOT or replace the signed invite');
});
});
@@ -0,0 +1,227 @@
/**
* Sprint 4A regression tests — MeshChat decomposition boundary checks.
*
* These tests validate the frozen contract:
* 1. High-privacy DM queueing lives in the controller
* 2. selectedGateAccessReady gating lives in the controller
* 3. DM polling trust-mutation code lives in the controller
* 4. Gate refresh is controller-owned via authenticated poll (SSE removed in S3A)
* 5. Identity persistence stays in meshIdentity.ts (not in presentational code)
* 6. No direct trust-mutating imports in presentational components
*/
import { describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
const MESH_CHAT_DIR = path.resolve(__dirname, '../../components/MeshChat');
function readFile(name: string): string {
return fs.readFileSync(path.join(MESH_CHAT_DIR, name), 'utf-8');
}
// ─── Trust-mutation isolation ───────────────────────────────────────────────
const TRUST_MUTATING_IMPORTS = [
'addContact',
'updateContact',
'blockContact',
'purgeBrowserSigningMaterial',
'purgeBrowserContactGraph',
'purgeBrowserDmState',
];
describe('MeshChat decomposition — trust mutation isolation', () => {
it('controller imports all trust-mutating functions', () => {
const controller = readFile('useMeshChatController.ts');
for (const fn of TRUST_MUTATING_IMPORTS) {
expect(controller).toContain(fn);
}
});
it('presentational index.tsx does NOT import trust-mutating functions directly', () => {
const index = readFile('index.tsx');
for (const fn of TRUST_MUTATING_IMPORTS) {
// Check that none of these appear in import statements
const importPattern = new RegExp(
`import\\s*\\{[^}]*\\b${fn}\\b[^}]*\\}\\s*from`,
);
expect(index).not.toMatch(importPattern);
}
});
it('presentational index.tsx does not import from meshIdentity', () => {
const index = readFile('index.tsx');
expect(index).not.toMatch(/from\s+['"]@\/mesh\/meshIdentity['"]/);
});
it('presentational index.tsx does not import from meshDmWorkerClient', () => {
const index = readFile('index.tsx');
expect(index).not.toMatch(/from\s+['"]@\/mesh\/meshDmWorkerClient['"]/);
});
});
// ─── Controller owns required-cohesion items ────────────────────────────────
describe('MeshChat decomposition — controller required-cohesion', () => {
const controller = readFile('useMeshChatController.ts');
it('controller exports enqueueDmSend (high-privacy DM queueing)', () => {
expect(controller).toMatch(/enqueueDmSend/);
// Also in the return block
expect(controller).toMatch(/return\s*\{[\s\S]*enqueueDmSend[\s\S]*\}/);
});
it('controller exports flushDmQueue (high-privacy DM queueing)', () => {
expect(controller).toMatch(/flushDmQueue/);
expect(controller).toMatch(/return\s*\{[\s\S]*flushDmQueue[\s\S]*\}/);
});
it('controller exports selectedGateAccessReady', () => {
expect(controller).toMatch(/selectedGateAccessReady/);
expect(controller).toMatch(/return\s*\{[\s\S]*selectedGateAccessReady[\s\S]*\}/);
});
it('controller exports selectedGateKeyStatus', () => {
expect(controller).toMatch(/selectedGateKeyStatus/);
expect(controller).toMatch(/return\s*\{[\s\S]*selectedGateKeyStatus[\s\S]*\}/);
});
it('controller exports native gate resync state and handler', () => {
expect(controller).toMatch(/gateResyncTarget/);
expect(controller).toMatch(/gateResyncBusy/);
expect(controller).toMatch(/handleResyncGateState/);
expect(controller).toMatch(/return\s*\{[\s\S]*gateResyncTarget[\s\S]*\}/);
expect(controller).toMatch(/return\s*\{[\s\S]*gateResyncBusy[\s\S]*\}/);
expect(controller).toMatch(/return\s*\{[\s\S]*handleResyncGateState[\s\S]*\}/);
});
it('controller exports secureDmBlocked', () => {
expect(controller).toMatch(/secureDmBlocked/);
expect(controller).toMatch(/return\s*\{[\s\S]*secureDmBlocked[\s\S]*\}/);
});
it('controller exports privacyProfile', () => {
expect(controller).toMatch(/privacyProfile/);
expect(controller).toMatch(/return\s*\{[\s\S]*privacyProfile[\s\S]*\}/);
});
it('controller exports hasId and hasPublicLaneIdentity', () => {
expect(controller).toMatch(/return\s*\{[\s\S]*hasId[\s\S]*\}/);
expect(controller).toMatch(/return\s*\{[\s\S]*hasPublicLaneIdentity[\s\S]*\}/);
});
it('controller exports publicMeshBlockedByWormhole', () => {
expect(controller).toMatch(/return\s*\{[\s\S]*publicMeshBlockedByWormhole[\s\S]*\}/);
});
it('controller exports anonymousPublicBlocked and anonymousDmBlocked', () => {
expect(controller).toMatch(/return\s*\{[\s\S]*anonymousPublicBlocked[\s\S]*\}/);
expect(controller).toMatch(/return\s*\{[\s\S]*anonymousDmBlocked[\s\S]*\}/);
});
});
// ─── Gate refresh is controller-owned (SSE removed in S3A) ────────────────
describe('MeshChat decomposition — gate refresh ownership', () => {
it('controller does NOT import useGateSSE (removed in S3A)', () => {
const controller = readFile('useMeshChatController.ts');
expect(controller).not.toMatch(/import.*useGateSSE.*from/);
expect(controller).not.toMatch(/useGateSSE\(/);
});
it('controller owns gate message polling via authenticated fetch', () => {
const controller = readFile('useMeshChatController.ts');
// The controller polls /api/mesh/infonet/messages for gate refresh
expect(controller).toMatch(/\/api\/mesh\/infonet\/messages/);
expect(controller).toMatch(/setInterval\(poll/);
});
it('useGateSSE is NOT imported in the presentational shell', () => {
const index = readFile('index.tsx');
expect(index).not.toMatch(/useGateSSE/);
});
});
// ─── DM polling trust unit controller-owned ─────────────────────────────────
describe('MeshChat decomposition — DM poll sequence in controller', () => {
const controller = readFile('useMeshChatController.ts');
it('DM polling (pollDmMailboxes) is in the controller', () => {
expect(controller).toMatch(/pollDmMailboxes/);
});
it('decryptDM is called in the controller (DM decrypt)', () => {
expect(controller).toMatch(/decryptDM/);
});
it('ratchetDecryptDM is in the controller', () => {
expect(controller).toMatch(/ratchetDecryptDM/);
});
it('sender seal decryption is in the controller via storage import', () => {
expect(controller).toMatch(/decryptSenderSealForContact/);
});
it('contact mutation (addContact/updateContact) happens only in controller', () => {
const index = readFile('index.tsx');
// These should not appear as direct function calls in the view
expect(index).not.toMatch(/\baddContact\s*\(/);
expect(index).not.toMatch(/\bupdateContact\s*\(/);
expect(index).not.toMatch(/\bblockContact\s*\(/);
});
});
// ─── Identity persistence through meshIdentity.ts ───────────────────────────
describe('MeshChat decomposition — identity persistence', () => {
it('controller imports identity functions from meshIdentity', () => {
const controller = readFile('useMeshChatController.ts');
expect(controller).toMatch(/from\s+['"]@\/mesh\/meshIdentity['"]/);
expect(controller).toMatch(/getNodeIdentity/);
expect(controller).toMatch(/generateNodeKeys/);
expect(controller).toMatch(/signEvent/);
});
it('storage module imports from meshIdentity for seal operations', () => {
const storage = readFile('storage.ts');
expect(storage).toMatch(/from\s+['"]@\/mesh\/meshIdentity['"]/);
});
it('types module re-exports Contact and NodeIdentity from meshIdentity', () => {
const types = readFile('types.ts');
expect(types).toMatch(/Contact/);
expect(types).toMatch(/NodeIdentity/);
});
});
// ─── Re-export stability ────────────────────────────────────────────────────
describe('MeshChat decomposition — export stability', () => {
it('MeshChat.tsx re-exports default from MeshChat/index', () => {
const reExport = fs.readFileSync(
path.resolve(MESH_CHAT_DIR, '../MeshChat.tsx'),
'utf-8',
);
expect(reExport).toMatch(/export\s*\{\s*default\s*\}\s*from\s+['"]\.\/MeshChat\/index['"]/);
});
it('MeshChat.tsx re-exports MeshChatProps type', () => {
const reExport = fs.readFileSync(
path.resolve(MESH_CHAT_DIR, '../MeshChat.tsx'),
'utf-8',
);
expect(reExport).toMatch(/export\s+type\s*\{\s*MeshChatProps\s*\}/);
});
it('index.tsx exports default MeshChat component', () => {
const index = readFile('index.tsx');
expect(index).toMatch(/export\s+default\s+MeshChat/);
});
it('presentational shell exposes the gate resync affordance', () => {
const index = readFile('index.tsx');
expect(index).toContain('RESYNC GATE STATE');
expect(index).toContain('handleResyncGateState(selectedGate)');
});
});
@@ -0,0 +1,179 @@
/**
* Phase 6A: Residual Backlog & Hygiene Closeout tests.
*
* Validates:
* 1. DECOY_KEY removal from types.ts causes no import/runtime regression
* 2. build_controller.py and build_index.py are deleted
* 3. promotePendingAlias no longer calls updateContact from storage.ts
* 4. Alias-promotion behavior unchanged after controller applies returned delta
*/
import { describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
const MESH_CHAT_DIR = path.resolve(__dirname, '../../components/MeshChat');
function readFile(name: string): string {
return fs.readFileSync(path.join(MESH_CHAT_DIR, name), 'utf-8');
}
function fileExists(name: string): boolean {
return fs.existsSync(path.join(MESH_CHAT_DIR, name));
}
// ---------------------------------------------------------------------------
// 1. DECOY_KEY removal causes no import/runtime regression
// ---------------------------------------------------------------------------
describe('DECOY_KEY deduplication', () => {
it('types.ts does NOT export DECOY_KEY', () => {
const types = readFile('types.ts');
expect(types).not.toMatch(/export\s+(const|let|var)\s+DECOY_KEY/);
});
it('storage.ts still exports DECOY_KEY as canonical location', () => {
const storage = readFile('storage.ts');
expect(storage).toMatch(/export\s+const\s+DECOY_KEY/);
});
it('DECOY_KEY is importable from storage at runtime', async () => {
const { DECOY_KEY } = await import('../../components/MeshChat/storage');
expect(DECOY_KEY).toBe('sb_dm_decoy');
});
it('no file imports DECOY_KEY from types', () => {
const files = fs.readdirSync(MESH_CHAT_DIR).filter((f) => f.endsWith('.ts') || f.endsWith('.tsx'));
for (const file of files) {
const content = readFile(file);
const importFromTypes = content.match(/import\s*\{[^}]*DECOY_KEY[^}]*\}\s*from\s*['"]\.\/types['"]/);
expect(importFromTypes, `${file} should not import DECOY_KEY from types`).toBeNull();
}
});
});
// ---------------------------------------------------------------------------
// 2. build_controller.py and build_index.py are deleted
// ---------------------------------------------------------------------------
describe('stale generator scripts removed', () => {
it('build_controller.py does not exist', () => {
expect(fileExists('build_controller.py')).toBe(false);
});
it('build_index.py does not exist', () => {
expect(fileExists('build_index.py')).toBe(false);
});
it('no build/test config references build_controller.py or build_index.py', () => {
const packageJson = fs.readFileSync(
path.resolve(MESH_CHAT_DIR, '../../../package.json'),
'utf-8',
);
expect(packageJson).not.toContain('build_controller.py');
expect(packageJson).not.toContain('build_index.py');
});
});
// ---------------------------------------------------------------------------
// 3. promotePendingAlias no longer calls updateContact from storage.ts
// ---------------------------------------------------------------------------
describe('promotePendingAlias decoupled from updateContact', () => {
it('storage.ts does not import updateContact', () => {
const storage = readFile('storage.ts');
expect(storage).not.toMatch(/import\s*\{[^}]*updateContact[^}]*\}\s*from/);
});
it('storage.ts does not import getContacts', () => {
const storage = readFile('storage.ts');
expect(storage).not.toMatch(/import\s*\{[^}]*getContacts[^}]*\}\s*from/);
});
it('promotePendingAlias does not call updateContact', () => {
const storage = readFile('storage.ts');
// Extract the promotePendingAlias function body
const fnStart = storage.indexOf('export function promotePendingAlias');
expect(fnStart).toBeGreaterThan(-1);
const fnBody = storage.slice(fnStart, storage.indexOf('\n}', fnStart) + 2);
expect(fnBody).not.toContain('updateContact(');
});
it('promotePendingAlias does not call getContacts', () => {
const storage = readFile('storage.ts');
const fnStart = storage.indexOf('export function promotePendingAlias');
const fnBody = storage.slice(fnStart, storage.indexOf('\n}', fnStart) + 2);
expect(fnBody).not.toContain('getContacts(');
});
it('controller call sites apply updateContact after promotePendingAlias', () => {
const controller = readFile('useMeshChatController.ts');
// Both call sites should follow pattern: promotePendingAlias → updateContact
const promotionCalls = controller.match(/const promotion = promotePendingAlias\(/g);
expect(promotionCalls?.length).toBeGreaterThanOrEqual(2);
const updateAfterPromotion = controller.match(
/if \(promotion\) updateContact\([^,]+, promotion\.delta\.updates\)/g,
);
expect(updateAfterPromotion?.length).toBeGreaterThanOrEqual(2);
});
});
// ---------------------------------------------------------------------------
// 4. Alias-promotion behavior unchanged (delta structure)
// ---------------------------------------------------------------------------
describe('alias-promotion delta correctness', () => {
it('returns null when contact has no pendingSharedAlias', async () => {
const { promotePendingAlias } = await import('../../components/MeshChat/storage');
const contact = { sharedAlias: 'abc' } as any;
const result = promotePendingAlias('test-id', contact);
expect(result).toBeNull();
});
it('returns null when grace period has not expired', async () => {
const { promotePendingAlias } = await import('../../components/MeshChat/storage');
const contact = {
pendingSharedAlias: 'new-alias',
sharedAlias: 'old-alias',
sharedAliasGraceUntil: Date.now() + 60_000,
} as any;
const result = promotePendingAlias('test-id', contact);
expect(result).toBeNull();
});
it('returns delta with promoted contact when grace period expired', async () => {
const { promotePendingAlias } = await import('../../components/MeshChat/storage');
const contact = {
pendingSharedAlias: 'new-alias',
sharedAlias: 'old-alias',
sharedAliasGraceUntil: Date.now() - 1000,
previousSharedAliases: [],
} as any;
const result = promotePendingAlias('test-id', contact);
expect(result).not.toBeNull();
expect(result!.delta.updates.sharedAlias).toBe('new-alias');
expect(result!.delta.updates.pendingSharedAlias).toBeUndefined();
expect(result!.delta.updates.sharedAliasGraceUntil).toBeUndefined();
expect(result!.delta.updates.sharedAliasRotatedAt).toBeGreaterThan(0);
expect(result!.delta.updates.previousSharedAliases).toContain('old-alias');
expect(result!.promoted.sharedAlias).toBe('new-alias');
expect(result!.promoted.pendingSharedAlias).toBeUndefined();
});
it('promoted contact merges updates onto original contact', async () => {
const { promotePendingAlias } = await import('../../components/MeshChat/storage');
const contact = {
dhPubKey: 'some-key',
pendingSharedAlias: 'next',
sharedAlias: 'current',
sharedAliasGraceUntil: 0,
previousSharedAliases: ['older'],
} as any;
const result = promotePendingAlias('test-id', contact);
expect(result).not.toBeNull();
// Original fields preserved
expect(result!.promoted.dhPubKey).toBe('some-key');
// Alias history includes both old aliases
expect(result!.promoted.previousSharedAliases).toContain('current');
expect(result!.promoted.previousSharedAliases).toContain('older');
});
});
@@ -121,6 +121,11 @@ describe('meshIdentity contact storage hardening', () => {
remotePrekeySequence: 3,
remotePrekeySignedAt: 444,
remotePrekeyMismatch: false,
remotePrekeyTransparencyHead: 'head-1',
remotePrekeyTransparencySize: 2,
remotePrekeyTransparencySeenAt: 555,
remotePrekeyTransparencyConflict: false,
remotePrekeyLookupMode: 'legacy_agent_id',
});
const stored = await waitForEncryptedContacts();
expect(String(stored ?? '')).toMatch(/^enc:/);
@@ -137,6 +142,11 @@ describe('meshIdentity contact storage hardening', () => {
expect(hydrated.alice.remotePrekeySequence).toBe(3);
expect(hydrated.alice.remotePrekeySignedAt).toBe(444);
expect(hydrated.alice.remotePrekeyMismatch).toBe(false);
expect(hydrated.alice.remotePrekeyTransparencyHead).toBe('head-1');
expect(hydrated.alice.remotePrekeyTransparencySize).toBe(2);
expect(hydrated.alice.remotePrekeyTransparencySeenAt).toBe(555);
expect(hydrated.alice.remotePrekeyTransparencyConflict).toBe(false);
expect(hydrated.alice.remotePrekeyLookupMode).toBe('legacy_agent_id');
});
it('migrates legacy plaintext contacts to encrypted storage on first hydrate', async () => {
@@ -241,4 +251,17 @@ describe('meshIdentity contact storage hardening', () => {
const rotated = await mailboxClaimToken('requests', '!sb_contacts123456');
expect(rotated).not.toBe(first);
});
it('rotates mailbox claim tokens across mailbox epochs', async () => {
const { mailboxClaimToken } = await import('@/mesh/meshMailbox');
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
const first = await mailboxClaimToken('requests', '!sb_contacts123456', 100);
const second = await mailboxClaimToken('requests', '!sb_contacts123456', 100);
const rotated = await mailboxClaimToken('requests', '!sb_contacts123456', 21_700);
expect(second).toBe(first);
expect(rotated).not.toBe(first);
});
});
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const deriveWormholeDeadDropTokens = vi.fn();
const deriveWormholeDeadDropTokenPair = vi.fn();
const isWormholeReady = vi.fn();
vi.mock('@/mesh/wormholeIdentityClient', () => ({
deriveWormholeDeadDropTokens,
deriveWormholeDeadDropTokenPair,
isWormholeReady,
}));
vi.mock('@/mesh/meshIdentity', () => ({
deriveSharedSecret: vi.fn(),
getStoredNodeDescriptor: vi.fn(() => ({ nodeId: 'local-node' })),
}));
describe('mesh dead-drop alias hygiene', () => {
beforeEach(() => {
deriveWormholeDeadDropTokens.mockReset();
deriveWormholeDeadDropTokenPair.mockReset();
isWormholeReady.mockReset();
});
it('sends alias refs instead of the stable peer id when mailbox aliases exist', async () => {
isWormholeReady.mockResolvedValue(true);
deriveWormholeDeadDropTokens.mockResolvedValue({
ok: true,
tokens: [
{ peer_id: 'peer_alpha', peer_ref: 'dmx_alpha', current: 'tok1', previous: 'tok0', epoch: 7 },
],
});
const { deadDropTokensForContacts } = await import('@/mesh/meshDeadDrop');
const tokens = await deadDropTokensForContacts(
{
peer_alpha: {
blocked: false,
dhPubKey: 'dhpub_alpha',
sharedAlias: 'dmx_alpha',
previousSharedAliases: ['dmx_prev_alpha'],
} as any,
},
24,
);
expect(tokens).toEqual(['tok1', 'tok0']);
expect(deriveWormholeDeadDropTokens).toHaveBeenCalledWith(
[
{
peer_id: 'peer_alpha',
peer_dh_pub: 'dhpub_alpha',
peer_refs: ['dmx_alpha', 'dmx_prev_alpha'],
},
],
24,
);
});
it('falls back to the stable peer id only when no alias history exists', async () => {
isWormholeReady.mockResolvedValue(true);
deriveWormholeDeadDropTokens.mockResolvedValue({
ok: true,
tokens: [
{ peer_id: 'peer_bravo', peer_ref: 'peer_bravo', current: 'tok2', previous: 'tok1', epoch: 8 },
],
});
const { deadDropTokensForContacts } = await import('@/mesh/meshDeadDrop');
await deadDropTokensForContacts(
{
peer_bravo: {
blocked: false,
dhPubKey: 'dhpub_bravo',
} as any,
},
24,
);
expect(deriveWormholeDeadDropTokens).toHaveBeenCalledWith(
[
{
peer_id: 'peer_bravo',
peer_dh_pub: 'dhpub_bravo',
peer_refs: ['peer_bravo'],
},
],
24,
);
});
});
@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
describe('fetchDmPublicKey lookup posture', () => {
const fetchMock = vi.fn();
beforeEach(() => {
fetchMock.mockReset();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('does not use legacy agent-id lookup unless explicitly allowed', async () => {
const mod = await import('@/mesh/meshDmClient');
const result = await mod.fetchDmPublicKey('http://localhost:8000', '!sb_legacy');
expect(result).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});
it('uses invite lookup handles without enabling legacy agent-id lookup', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'invite_lookup_handle' }),
});
const mod = await import('@/mesh/meshDmClient');
const result = await mod.fetchDmPublicKey(
'http://localhost:8000',
'!sb_peer',
'invite-handle-123',
);
expect(result?.dh_pub_key).toBe('peer-dh');
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:8000/api/mesh/dm/pubkey?lookup_token=invite-handle-123',
);
});
it('still supports explicit legacy agent-id lookup for migration-only paths', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'legacy_agent_id' }),
});
const mod = await import('@/mesh/meshDmClient');
const result = await mod.fetchDmPublicKey('http://localhost:8000', '!sb_legacy', undefined, {
allowLegacyAgentId: true,
});
expect(result?.dh_pub_key).toBe('peer-dh');
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:8000/api/mesh/dm/pubkey?agent_id=%21sb_legacy',
);
});
});
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
allDmPeerIds,
mailboxPeerRefs,
buildAliasRotateMessage,
buildAccessGrantedMessage,
buildContactAcceptMessage,
@@ -59,6 +60,17 @@ describe('mesh DM consent helpers', () => {
expect(allDmPeerIds('node_public', { sharedAlias: 'node_public' })).toEqual(['node_public']);
});
it('prefers alias history for mailbox refs and drops stable public id once aliasing exists', () => {
expect(
mailboxPeerRefs('node_public', {
sharedAlias: 'dmx_current',
pendingSharedAlias: 'dmx_next',
previousSharedAliases: ['dmx_prev'],
}),
).toEqual(['dmx_current', 'dmx_next', 'dmx_prev']);
expect(mailboxPeerRefs('node_public', { sharedAlias: '' })).toEqual(['node_public']);
});
it('builds and parses alias rotation control payloads', () => {
const message = buildAliasRotateMessage('dmx_next');
expect(parseAliasRotateMessage(message)).toEqual({ shared_alias: 'dmx_next' });
@@ -76,10 +88,38 @@ describe('mesh DM consent helpers', () => {
});
it('keeps alias history compact and unique', () => {
expect(mergeAliasHistory(['dmx_a', 'dmx_b', 'dmx_a', 'dmx_c', 'dmx_d'])).toEqual([
'dmx_a',
'dmx_b',
]);
expect(mergeAliasHistory(['dmx_a', 'dmx_b', 'dmx_a', 'dmx_c', 'dmx_d'], 3)).toEqual([
'dmx_a',
'dmx_b',
'dmx_c',
]);
});
it('bounds mailbox peer refs to 4 and excludes long tail', () => {
expect(
mailboxPeerRefs('node_public', {
sharedAlias: 'dmx_current',
pendingSharedAlias: 'dmx_next',
previousSharedAliases: ['dmx_prev1', 'dmx_prev2', 'dmx_prev3'],
}),
).toEqual(['dmx_current', 'dmx_next', 'dmx_prev1', 'dmx_prev2']);
});
it('bounds allDmPeerIds previous alias enumeration to 2', () => {
const ids = allDmPeerIds('node_public', {
sharedAlias: 'dmx_current',
pendingSharedAlias: 'dmx_next',
previousSharedAliases: ['dmx_prev1', 'dmx_prev2', 'dmx_prev3'],
});
// current + pending + at most 2 previous + peerId
expect(ids).toContain('dmx_current');
expect(ids).toContain('dmx_next');
expect(ids).toContain('dmx_prev1');
expect(ids).toContain('dmx_prev2');
expect(ids).not.toContain('dmx_prev3');
});
});
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const signMeshEvent = vi.fn();
const issueWormholeDmSenderToken = vi.fn();
const issueWormholeDmSenderTokens = vi.fn();
const registerWormholeDmKey = vi.fn();
const validateEventPayload = vi.fn(() => ({ ok: true, reason: 'ok' }));
const nextSequence = vi.fn(() => 42);
vi.mock('@/mesh/meshDeadDrop', () => ({
deadDropToken: vi.fn(async () => 'shared-token'),
deadDropTokensForContacts: vi.fn(async () => []),
}));
vi.mock('@/mesh/meshMailbox', () => ({
mailboxClaimToken: vi.fn(async (type: string) => `${type}-token`),
mailboxDecoySharedToken: vi.fn(async (index: number) => `decoy-${index}`),
}));
vi.mock('@/mesh/meshIdentity', () => ({
deriveSenderSealKey: vi.fn(),
ensureDhKeysFresh: vi.fn(),
deriveSharedKey: vi.fn(),
encryptDM: vi.fn(),
getDHAlgo: vi.fn(() => 'X25519'),
getNodeIdentity: vi.fn(() => ({ nodeId: '!sb_self', publicKey: 'pub' })),
getPublicKeyAlgo: vi.fn(() => 'Ed25519'),
nextSequence,
verifyNodeIdBindingFromPublicKey: vi.fn(async () => true),
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
buildWormholeSenderSeal: vi.fn(),
getActiveSigningContext: vi.fn(async () => null),
isWormholeSecureRequired: vi.fn(async () => false),
issueWormholeDmSenderToken,
issueWormholeDmSenderTokens,
registerWormholeDmKey,
signRawMeshMessage: vi.fn(),
signMeshEvent,
}));
vi.mock('@/mesh/meshSchema', () => ({
validateEventPayload,
}));
describe('DM transport lock signing', () => {
const fetchMock = vi.fn();
const identity = {
nodeId: '!sb_self',
publicKey: 'pub',
privateKey: 'priv',
};
beforeEach(() => {
vi.resetModules();
fetchMock.mockReset();
vi.stubGlobal('fetch', fetchMock);
validateEventPayload.mockClear();
nextSequence.mockClear();
signMeshEvent.mockReset();
issueWormholeDmSenderToken.mockReset();
issueWormholeDmSenderTokens.mockReset();
registerWormholeDmKey.mockReset();
signMeshEvent.mockResolvedValue({
context: {
nodeId: '!sb_self',
publicKey: 'pub',
publicKeyAlgo: 'Ed25519',
},
signature: 'sig',
sequence: 42,
protocolVersion: 'infonet/2',
});
issueWormholeDmSenderTokens.mockResolvedValue({ tokens: [] });
issueWormholeDmSenderToken.mockResolvedValue({ sender_token: 'sender-token' });
registerWormholeDmKey.mockResolvedValue({ ok: true });
fetchMock.mockResolvedValue({ json: async () => ({ ok: true }) });
});
it('signs and sends private_strong on DM sends', async () => {
const mod = await import('@/mesh/meshDmClient');
await mod.sendDmMessage({
apiBase: 'http://localhost:8000',
identity,
recipientId: '!sb_peer',
ciphertext: 'sealed',
msgId: 'dm-test-1',
timestamp: 123,
deliveryClass: 'request',
});
expect(signMeshEvent).toHaveBeenCalledWith(
'dm_message',
expect.objectContaining({ transport_lock: 'private_strong' }),
42,
);
const body = JSON.parse(fetchMock.mock.calls.at(-1)?.[1]?.body as string);
expect(body.transport_lock).toBe('private_strong');
});
it('signs and sends private_strong on DM poll/count', async () => {
const mod = await import('@/mesh/meshDmClient');
const claims = [{ type: 'requests' as const, token: 'request-token' }];
await mod.pollDmMailboxes('http://localhost:8000', identity, claims);
await mod.countDmMailboxes('http://localhost:8000', identity, claims);
expect(signMeshEvent).toHaveBeenCalledWith(
'dm_poll',
expect.objectContaining({ transport_lock: 'private_strong' }),
42,
);
expect(signMeshEvent).toHaveBeenCalledWith(
'dm_count',
expect.objectContaining({ transport_lock: 'private_strong' }),
42,
);
const pollBody = JSON.parse(fetchMock.mock.calls[0][1].body as string);
const countBody = JSON.parse(fetchMock.mock.calls[1][1].body as string);
expect(pollBody.transport_lock).toBe('private_strong');
expect(countBody.transport_lock).toBe('private_strong');
});
});
@@ -0,0 +1,328 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
const probeInlineGateCryptoSupport = vi.fn(async () => ({ supported: true, reason: '' }));
const adoptInlineGateState = vi.fn(async (snapshot) => snapshot);
const composeInlineGateMessage = vi.fn(async () => ({
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
}));
const decryptInlineGateMessages = vi.fn(async () => [
{
ok: true,
gate_id: 'infonet',
epoch: 7,
plaintext: 'sealed',
reply_to: '',
identity_scope: 'browser_privacy_core',
},
]);
const forgetInlineGateState = vi.fn(async () => {});
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
vi.mock('@/mesh/meshGateLocalRuntime', () => ({
probeInlineGateCryptoSupport,
adoptInlineGateState,
composeInlineGateMessage,
decryptInlineGateMessages,
forgetInlineGateState,
}));
describe('meshGateWorkerClient inline fallback', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneJson.mockReset();
probeInlineGateCryptoSupport.mockReset();
adoptInlineGateState.mockReset();
composeInlineGateMessage.mockReset();
decryptInlineGateMessages.mockReset();
forgetInlineGateState.mockReset();
probeInlineGateCryptoSupport.mockResolvedValue({ supported: true, reason: '' });
adoptInlineGateState.mockImplementation(async (snapshot) => snapshot);
composeInlineGateMessage.mockResolvedValue({
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
});
decryptInlineGateMessages.mockResolvedValue([
{
ok: true,
gate_id: 'infonet',
epoch: 7,
plaintext: 'sealed',
reply_to: '',
identity_scope: 'browser_privacy_core',
},
]);
forgetInlineGateState.mockResolvedValue(undefined);
Object.defineProperty(globalThis, 'Worker', {
value: undefined,
configurable: true,
writable: true,
});
});
it('uses the inline runtime when the Worker transport is unavailable', async () => {
controlPlaneJson
.mockResolvedValueOnce({
gate_id: 'infonet',
epoch: 7,
rust_state_blob_b64: 'blob',
members: [],
active_identity_scope: 'anonymous',
active_persona_id: '',
active_node_id: '!sb_local',
})
.mockResolvedValueOnce({
ok: true,
gate_id: 'infonet',
sender_id: '!sb_gate',
public_key: 'pub',
public_key_algo: 'ed25519',
protocol_version: 'sb-test',
sequence: 3,
signature: 'sig',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
sender_ref: 'sender-ref',
format: 'mls1',
});
const mod = await import('@/mesh/meshGateWorkerClient');
await expect(mod.syncBrowserGateState('infonet', { force: true })).resolves.toBe(true);
await expect(mod.composeBrowserGateMessage('infonet', 'hello')).resolves.toEqual(
expect.objectContaining({
ok: true,
gate_id: 'infonet',
ciphertext: 'inline-ciphertext',
}),
);
await expect(
mod.decryptBrowserGateMessages([
{
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
},
]),
).resolves.toEqual({
ok: true,
results: [
expect.objectContaining({
ok: true,
gate_id: 'infonet',
plaintext: 'sealed',
}),
],
});
expect(probeInlineGateCryptoSupport).toHaveBeenCalled();
expect(adoptInlineGateState).toHaveBeenCalled();
expect(composeInlineGateMessage).toHaveBeenCalledWith('infonet', 'hello', '');
expect(decryptInlineGateMessages).toHaveBeenCalledWith([
{
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
},
]);
expect(mod.getBrowserGateLocalRuntimeStatus()).toEqual(
expect.objectContaining({
mode: 'inline',
health: 'active',
reason: 'browser_gate_worker_unavailable',
}),
);
expect(mod.describeBrowserGateLocalRuntimeStatus(mod.getBrowserGateLocalRuntimeStatus())).toBe(
'INLINE local gate runtime active (worker unavailable)',
);
expect(controlPlaneJson).toHaveBeenNthCalledWith(
1,
'/api/wormhole/gate/state/export',
expect.anything(),
);
expect(controlPlaneJson).toHaveBeenNthCalledWith(
2,
'/api/wormhole/gate/message/sign-encrypted',
expect.objectContaining({
body: JSON.stringify({
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
format: 'mls1',
reply_to: '',
compat_reply_to: false,
recovery_plaintext: 'hello',
}),
}),
);
});
it('falls back to backend sealing when browser signing cannot return a durable gate envelope', async () => {
controlPlaneJson
.mockResolvedValueOnce({
gate_id: 'infonet',
epoch: 7,
rust_state_blob_b64: 'blob',
members: [],
active_identity_scope: 'anonymous',
active_persona_id: '',
active_node_id: '!sb_local',
})
.mockResolvedValueOnce({
ok: true,
gate_id: 'infonet',
sender_id: '!sb_gate',
public_key: 'pub',
public_key_algo: 'ed25519',
protocol_version: 'sb-test',
sequence: 3,
signature: 'sig',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
sender_ref: 'sender-ref',
format: 'mls1',
})
.mockResolvedValueOnce({
ok: true,
event_id: 'evt-backend-sealed',
});
const mod = await import('@/mesh/meshGateWorkerClient');
await expect(mod.syncBrowserGateState('infonet', { force: true })).resolves.toBe(true);
await expect(mod.postBrowserGateMessage('infonet', 'hello durable', 'evt-parent-1')).resolves.toEqual({
ok: true,
event_id: 'evt-backend-sealed',
});
expect(controlPlaneJson).toHaveBeenNthCalledWith(
3,
'/api/wormhole/gate/message/post',
expect.objectContaining({
body: JSON.stringify({
gate_id: 'infonet',
plaintext: 'hello durable',
reply_to: 'evt-parent-1',
compat_plaintext: true,
}),
}),
);
});
it('marks the selected inline runtime as degraded when a later local compose fails', async () => {
controlPlaneJson.mockResolvedValueOnce({
gate_id: 'infonet',
epoch: 7,
rust_state_blob_b64: 'blob',
members: [],
active_identity_scope: 'anonymous',
active_persona_id: '',
active_node_id: '!sb_local',
});
composeInlineGateMessage.mockRejectedValueOnce(new Error('worker_gate_wrap_key_missing'));
const mod = await import('@/mesh/meshGateWorkerClient');
await expect(mod.syncBrowserGateState('infonet', { force: true })).resolves.toBe(true);
await expect(mod.composeBrowserGateMessage('infonet', 'hello')).resolves.toBeNull();
expect(mod.getBrowserGateCryptoFailureReason('infonet', 'compose')).toBe('worker_gate_wrap_key_missing');
expect(mod.getBrowserGateLocalRuntimeStatus()).toEqual(
expect.objectContaining({
mode: 'inline',
health: 'degraded',
reason: 'worker_gate_wrap_key_missing',
}),
);
expect(mod.describeBrowserGateLocalRuntimeStatus(mod.getBrowserGateLocalRuntimeStatus())).toBe(
'INLINE local gate runtime degraded (secure storage unavailable)',
);
});
it('reuses self-authored plaintext when local gate decrypt cannot reopen the just-posted ciphertext', async () => {
controlPlaneJson
.mockResolvedValueOnce({
gate_id: 'infonet',
epoch: 7,
rust_state_blob_b64: 'blob',
members: [],
active_identity_scope: 'anonymous',
active_persona_id: '',
active_node_id: '!sb_local',
})
.mockResolvedValueOnce({
ok: true,
gate_id: 'infonet',
sender_id: '!sb_gate',
public_key: 'pub',
public_key_algo: 'ed25519',
protocol_version: 'sb-test',
sequence: 3,
signature: 'sig',
epoch: 7,
ciphertext: 'inline-ciphertext',
nonce: 'inline-nonce',
sender_ref: 'sender-ref',
format: 'mls1',
});
decryptInlineGateMessages.mockResolvedValueOnce([
{
ok: false,
gate_id: 'infonet',
detail: 'gate_mls_decrypt_failed',
},
]);
const mod = await import('@/mesh/meshGateWorkerClient');
await expect(mod.syncBrowserGateState('infonet', { force: true })).resolves.toBe(true);
await expect(mod.composeBrowserGateMessage('infonet', 'hello self', 'evt-parent-7')).resolves.toEqual(
expect.objectContaining({
ok: true,
gate_id: 'infonet',
ciphertext: 'inline-ciphertext',
}),
);
await expect(
mod.decryptBrowserGateMessages([
{
gate_id: 'infonet',
epoch: 7,
ciphertext: 'inline-ciphertext',
},
]),
).resolves.toEqual({
ok: true,
results: [
expect.objectContaining({
ok: true,
gate_id: 'infonet',
epoch: 7,
plaintext: 'hello self',
reply_to: 'evt-parent-7',
identity_scope: 'browser_self_echo',
}),
],
});
expect(mod.getBrowserGateLocalRuntimeStatus()).toEqual(
expect.objectContaining({
mode: 'inline',
health: 'active',
}),
);
});
});
@@ -0,0 +1,226 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
type StoreRecord = Map<string, unknown>;
type DbRecord = {
version: number;
stores: Map<string, StoreRecord>;
};
const databases = new Map<string, DbRecord>();
const deletedDatabases: string[] = [];
function domStringList(record: DbRecord): DOMStringList {
return {
contains: (name: string) => record.stores.has(name),
item: (index: number) => Array.from(record.stores.keys())[index] ?? null,
get length() {
return record.stores.size;
},
} as DOMStringList;
}
function makeRequest<T>(
executor: (request: IDBRequest<T>) => void,
tx?: IDBTransaction,
): IDBRequest<T> {
const request = {} as IDBRequest<T>;
queueMicrotask(() => {
executor(request);
tx?.oncomplete?.(new Event('complete') as Event);
});
return request;
}
function makeObjectStore(record: DbRecord, name: string, tx: IDBTransaction): IDBObjectStore {
const store = record.stores.get(name);
if (!store) throw new Error(`missing object store ${name}`);
return {
get(key: IDBValidKey) {
return makeRequest((request) => {
(request as { result?: unknown }).result = store.get(String(key));
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
put(value: unknown, key?: IDBValidKey) {
return makeRequest((request) => {
store.set(String(key ?? ''), value);
(request as { result?: unknown }).result = key;
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
delete(key: IDBValidKey) {
return makeRequest((request) => {
store.delete(String(key));
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
clear() {
return makeRequest((request) => {
store.clear();
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
} as unknown as IDBObjectStore;
}
function makeTransaction(record: DbRecord): IDBTransaction {
const tx = {
oncomplete: null,
onerror: null,
onabort: null,
objectStore: (name: string) => makeObjectStore(record, name, tx as unknown as IDBTransaction),
} as unknown as IDBTransaction;
return tx;
}
function makeDb(name: string, record: DbRecord): IDBDatabase {
return {
name,
version: record.version,
objectStoreNames: domStringList(record),
createObjectStore(storeName: string) {
if (!record.stores.has(storeName)) {
record.stores.set(storeName, new Map());
}
return {} as IDBObjectStore;
},
transaction(_storeName: string | string[]) {
return makeTransaction(record);
},
close() {
/* noop */
},
} as unknown as IDBDatabase;
}
function createFakeIndexedDb() {
return {
open(name: string, version?: number) {
const request = {} as IDBOpenDBRequest;
queueMicrotask(() => {
const resolvedVersion = Number(version || 1);
let record = databases.get(name);
const upgrading = !record || resolvedVersion > record.version;
if (!record) {
record = { version: resolvedVersion, stores: new Map() };
databases.set(name, record);
}
if (upgrading) {
record.version = resolvedVersion;
(request as { result?: IDBDatabase }).result = makeDb(name, record);
request.onupgradeneeded?.(new Event('upgradeneeded') as IDBVersionChangeEvent);
}
(request as { result?: IDBDatabase }).result = makeDb(name, record);
request.onsuccess?.(new Event('success') as Event);
});
return request;
},
deleteDatabase(name: string) {
const request = {} as IDBOpenDBRequest;
queueMicrotask(() => {
deletedDatabases.push(name);
databases.delete(name);
request.onsuccess?.(new Event('success') as Event);
});
return request;
},
};
}
function ensureStore(name: string, version: number, storeName: string): StoreRecord {
let record = databases.get(name);
if (!record) {
record = { version, stores: new Map() };
databases.set(name, record);
}
record.version = Math.max(record.version, version);
if (!record.stores.has(storeName)) {
record.stores.set(storeName, new Map());
}
return record.stores.get(storeName)!;
}
function getStoredValue(name: string, storeName: string, key: string): unknown {
return databases.get(name)?.stores.get(storeName)?.get(key);
}
describe('gate worker vault hardening', () => {
beforeEach(() => {
vi.resetModules();
databases.clear();
deletedDatabases.length = 0;
Object.defineProperty(globalThis, 'indexedDB', {
value: createFakeIndexedDb(),
configurable: true,
writable: true,
});
});
it('persists worker gate state as an encrypted blob instead of raw state', async () => {
const mod = await import('@/mesh/meshGateWorkerVault');
const sample = {
gate_id: 'infonet',
epoch: 7,
rust_state_blob_b64: 'blob-private',
members: [
{
persona_id: 'persona-a',
node_id: '!sb_gate',
identity_scope: 'persona',
group_handle: 11,
},
],
active_identity_scope: 'persona',
active_persona_id: 'persona-a',
active_node_id: '!sb_gate',
};
await mod.writeWorkerGateState(sample);
const raw = getStoredValue(mod.WORKER_GATE_DB, 'gate_state', 'infonet');
expect(typeof raw).toBe('string');
expect(String(raw)).not.toContain('blob-private');
expect(String(raw)).not.toContain('persona-a');
const loaded = await mod.readWorkerGateState('infonet');
expect(loaded).toEqual(sample);
});
it('migrates legacy plaintext gate state into encrypted storage on read', async () => {
const legacyStore = ensureStore('sb_mesh_gate_worker', 1, 'gate_state');
ensureStore('sb_mesh_gate_worker', 1, 'meta');
legacyStore.set('infonet', {
gate_id: 'infonet',
epoch: 4,
rust_state_blob_b64: 'legacy-blob',
members: [],
active_identity_scope: 'anonymous',
active_persona_id: '',
active_node_id: '!sb_legacy',
});
const mod = await import('@/mesh/meshGateWorkerVault');
const loaded = await mod.readWorkerGateState('infonet');
const raw = getStoredValue(mod.WORKER_GATE_DB, 'gate_state', 'infonet');
expect(loaded?.rust_state_blob_b64).toBe('legacy-blob');
expect(typeof raw).toBe('string');
expect(String(raw)).not.toContain('legacy-blob');
});
it('drops stale encrypted gate state when the wrap key is missing so the room can resync cleanly', async () => {
const gateStore = ensureStore('sb_mesh_gate_worker', 1, 'gate_state');
gateStore.set('infonet', 'encrypted-state-that-cannot-be-opened');
ensureStore('sb_mesh_gate_worker', 1, 'meta');
const mod = await import('@/mesh/meshGateWorkerVault');
await expect(mod.readWorkerGateState('infonet')).resolves.toBeNull();
expect(getStoredValue(mod.WORKER_GATE_DB, 'gate_state', 'infonet')).toBeUndefined();
});
it('deleteWorkerGateDatabase removes the persisted gate vault', async () => {
const mod = await import('@/mesh/meshGateWorkerVault');
await mod.deleteWorkerGateDatabase();
expect(deletedDatabases).toContain('sb_mesh_gate_worker');
});
});
@@ -80,10 +80,11 @@ describe('mesh identity storage separation', () => {
expect(mod.getWormholeIdentityDescriptor()).toBeNull();
});
it('migrates legacy browser and Wormhole node ids to the current format', async () => {
it('migrates stored browser and Wormhole node ids from 8-hex and 16-hex forms', async () => {
const mod = await import('@/mesh/meshIdentity');
const publicKey = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
const currentNodeId = await mod.deriveNodeIdFromPublicKey(publicKey);
const compatNodeId = currentNodeId.slice(0, '!sb_'.length + 16);
mod.cachePublicIdentity({
nodeId: '!sb_deadbeef',
@@ -108,5 +109,42 @@ describe('mesh identity storage separation', () => {
publicKey,
publicKeyAlgo: 'Ed25519',
});
mod.cachePublicIdentity({
nodeId: compatNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
mod.cacheWormholeIdentityDescriptor({
nodeId: compatNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
await mod.migrateLegacyNodeIds();
expect(mod.getStoredNodeDescriptor()).toEqual({
nodeId: currentNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
expect(mod.getWormholeIdentityDescriptor()).toEqual({
nodeId: currentNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
});
it('accepts 32-hex current node ids and 16-hex compatibility ids, but not 8-hex ids', async () => {
const mod = await import('@/mesh/meshIdentity');
const publicKey = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
const currentNodeId = await mod.deriveNodeIdFromPublicKey(publicKey);
const compatNodeId = currentNodeId.slice(0, '!sb_'.length + 16);
await expect(mod.verifyNodeIdBindingFromPublicKey(publicKey, currentNodeId)).resolves.toBe(true);
await expect(mod.verifyNodeIdBindingFromPublicKey(publicKey, compatNodeId)).resolves.toBe(true);
await expect(mod.verifyNodeIdBindingFromPublicKey(publicKey, '!sb_deadbeef')).resolves.toBe(
false,
);
});
});
@@ -4,7 +4,11 @@ import {
buildDmTrustHint,
buildPrivateLaneHint,
dmTrustPrimaryActionLabel,
hasKnownFirstContactAnchor,
hasVerifiedFirstContactAnchor,
isInvitePinnedFirstContact,
isFirstContactTrustOnly,
requiresVerifiedFirstContact,
shortTrustFingerprint,
shouldAutoRevealSasForTrust,
} from '@/mesh/meshPrivacyHints';
@@ -61,6 +65,149 @@ describe('meshPrivacyHints', () => {
expect(shouldAutoRevealSasForTrust(contact)).toBe(true);
});
it('treats invite-pinned first contact as stronger than TOFU', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'anchored by signed invite',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
rootWitnessThreshold: 2,
rootWitnessCount: 2,
rootWitnessDomainCount: 1,
rootWitnessProvenanceState: 'local_quorum',
rootWitnessIndependentQuorumMet: false,
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(isInvitePinnedFirstContact(contact)).toBe(true);
expect(isFirstContactTrustOnly(contact)).toBe(false);
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'ROOT LOCAL QUORUM',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('co-resident in one trust domain');
expect(dmTrustPrimaryActionLabel(contact)).toBe('SHOW SAS');
expect(shouldAutoRevealSasForTrust(contact)).toBe(false);
});
it('distinguishes independent quorum provenance from local quorum', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'anchored by signed invite on independent quorum root',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
rootWitnessThreshold: 2,
rootWitnessCount: 2,
rootWitnessDomainCount: 2,
rootWitnessProvenanceState: 'independent_quorum',
rootWitnessIndependentQuorumMet: true,
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'ROOT INDEPENDENT QUORUM',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('independently quorum-witnessed');
});
it('requires verified first-contact anchors before secure bootstrap', () => {
expect(requiresVerifiedFirstContact(undefined)).toBe(true);
expect(hasKnownFirstContactAnchor(undefined)).toBe(false);
expect(hasVerifiedFirstContactAnchor(undefined)).toBe(false);
expect(
requiresVerifiedFirstContact({
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'anchored by signed invite',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
registryMismatch: false,
transparencyConflict: false,
},
}),
).toBe(false);
expect(
hasVerifiedFirstContactAnchor({
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'anchored by signed invite',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
registryMismatch: false,
transparencyConflict: false,
},
}),
).toBe(true);
expect(
requiresVerifiedFirstContact({
remotePrekeyFingerprint: 'abc123',
remotePrekeyPinnedAt: 123,
}),
).toBe(true);
expect(
hasVerifiedFirstContactAnchor({
remotePrekeyFingerprint: 'abc123',
remotePrekeyPinnedAt: 123,
}),
).toBe(false);
expect(
requiresVerifiedFirstContact({
verified: true,
verify_inband: true,
verify_registry: true,
}),
).toBe(true);
expect(
hasVerifiedFirstContactAnchor({
verified: true,
verify_inband: true,
verify_registry: true,
}),
).toBe(false);
});
it('auto-reveals SAS for trust hazards but keeps ordinary verified contacts quiet', () => {
expect(
shouldAutoRevealSasForTrust({
@@ -74,20 +221,295 @@ describe('meshPrivacyHints', () => {
).toBe(true);
expect(
shouldAutoRevealSasForTrust({
verified: true,
verify_inband: true,
verify_registry: true,
trustSummary: {
state: 'sas_verified',
label: 'SAS VERIFIED',
severity: 'good',
detail: 'sas verified',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: false,
rootDistributionState: 'none',
registryMismatch: false,
transparencyConflict: false,
},
}),
).toBe(false);
expect(
dmTrustPrimaryActionLabel({
verified: true,
verify_inband: true,
verify_registry: true,
trustSummary: {
state: 'sas_verified',
label: 'SAS VERIFIED',
severity: 'good',
detail: 'sas verified',
verifiedFirstContact: true,
recommendedAction: 'show_sas',
legacyLookup: false,
inviteAttested: false,
rootDistributionState: 'none',
registryMismatch: false,
transparencyConflict: false,
},
}),
).toBe('SHOW SAS');
});
it('maps import-invite and reverify actions to distinct labels', () => {
expect(
dmTrustPrimaryActionLabel({
trustSummary: {
state: 'unpinned',
label: 'UNVERIFIED',
severity: 'warn',
detail: 'invite required',
verifiedFirstContact: false,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: false,
registryMismatch: false,
transparencyConflict: false,
},
}),
).toBe('IMPORT INVITE');
expect(
dmTrustPrimaryActionLabel({
trustSummary: {
state: 'continuity_broken',
label: 'CONTINUITY BROKEN',
severity: 'danger',
detail: 'reverify',
verifiedFirstContact: false,
recommendedAction: 'reverify',
legacyLookup: false,
inviteAttested: true,
registryMismatch: true,
transparencyConflict: false,
},
}),
).toBe('REVERIFY NOW');
});
it('surfaces stable root mismatch as a continuity hazard', () => {
const contact = {
trustSummary: {
state: 'continuity_broken',
label: 'CONTINUITY BROKEN',
severity: 'danger',
detail: 'root changed',
verifiedFirstContact: false,
recommendedAction: 'reverify',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
rootMismatch: true,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'danger',
title: 'CONTINUITY BROKEN',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('stable root identity');
});
it('treats legacy lookup on an otherwise verified contact as an invite-import migration state', () => {
const contact = {
trustSummary: {
state: 'sas_verified',
label: 'SAS VERIFIED',
severity: 'good',
detail: 'sas verified but still legacy lookup',
verifiedFirstContact: true,
recommendedAction: 'import_invite',
legacyLookup: true,
inviteAttested: false,
rootDistributionState: 'none',
registryMismatch: false,
transparencyConflict: false,
},
};
expect(dmTrustPrimaryActionLabel(contact)).toBe('IMPORT INVITE');
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'LEGACY LOOKUP',
}),
);
});
it('surfaces internal-only root continuity as an invite refresh state', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'invite pinned on internal root only',
verifiedFirstContact: true,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: false,
rootDistributionState: 'internal_only',
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(dmTrustPrimaryActionLabel(contact)).toBe('IMPORT INVITE');
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'ROOT INTERNAL ONLY',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('witnessed root');
});
it('surfaces single-witness root continuity as a weaker witnessed state', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'invite pinned on single witness root',
verifiedFirstContact: true,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'single_witness',
rootWitnessCount: 1,
rootWitnessThreshold: 1,
rootWitnessQuorumMet: true,
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(dmTrustPrimaryActionLabel(contact)).toBe('IMPORT INVITE');
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'ROOT SINGLE WITNESS',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('quorum witness provenance');
});
it('surfaces unproven witnessed root rotation as a hard invite refresh state', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'invite pinned on witnessed root without rotation proof',
verifiedFirstContact: false,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'quorum_witnessed',
rootManifestGeneration: 2,
rootRotationProven: false,
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(dmTrustPrimaryActionLabel(contact)).toBe('IMPORT INVITE');
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'danger',
title: 'ROOT ROTATION UNPROVEN',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('previous-root proof');
});
it('surfaces unsatisfied witness policy as a hard invite refresh state', () => {
const contact = {
trustSummary: {
state: 'invite_pinned',
label: 'INVITE PINNED',
severity: 'warn',
detail: 'invite pinned on root missing witness quorum',
verifiedFirstContact: false,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: true,
rootAttested: true,
rootWitnessed: true,
rootDistributionState: 'witness_policy_not_met',
rootWitnessCount: 1,
rootWitnessThreshold: 2,
rootWitnessQuorumMet: false,
rootMismatch: false,
registryMismatch: false,
transparencyConflict: false,
},
};
expect(dmTrustPrimaryActionLabel(contact)).toBe('IMPORT INVITE');
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'danger',
title: 'ROOT WITNESS POLICY NOT MET',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('witness policy');
});
it('transitional lane hint separates gate posture from DM posture', () => {
const hint = buildPrivateLaneHint({
activeTab: 'infonet',
privateInfonetReady: true,
privateInfonetTransportReady: false,
});
expect(hint).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'TRANSITIONAL PRIVATE LANE',
}),
);
// Must explicitly mention gate is on a transitional lane
expect(hint?.detail).toContain('transitional');
// Must explicitly mention DM requires a stronger tier
expect(hint?.detail).toContain('Dead Drop');
expect(hint?.detail).toMatch(/PRIVATE \/ STRONG/i);
// Must not imply gate and DM share the same posture
expect(hint?.detail).toContain('weaker than DM');
});
it('relay delivery hint is specific to Dead Drop, not gate', () => {
const hint = buildPrivateLaneHint({
activeTab: 'dms',
dmTransportMode: 'relay',
});
expect(hint).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'RELAY DELIVERY ACTIVE',
}),
);
expect(hint?.detail).toContain('Dead Drop');
});
it('shortens long trust fingerprints for display', () => {
expect(shortTrustFingerprint('abcdef0123456789fedcba9876543210')).toBe('abcdef01..543210');
expect(shortTrustFingerprint('abcd1234')).toBe('abcd1234');
@@ -1,3 +1,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';
import {
@@ -32,10 +35,59 @@ describe('mesh terminal policy', () => {
expect(isMeshTerminalWriteCommand('send', ['broadcast', 'hello'])).toBe(true);
});
it('wormhole active lock reason distinguishes gate and DM posture', () => {
const reason = getMeshTerminalWriteLockReason({
wormholeRequired: true,
wormholeReady: true,
anonymousMode: false,
anonymousModeReady: false,
});
// Must mention gate as transitional lane
expect(reason).toContain('gate chat (transitional lane)');
// Must mention Dead Drop as the stronger lane
expect(reason).toContain('Dead Drop (stronger private lane)');
// Must NOT use "hardened private actions" which flattens both
expect(reason).not.toContain('hardened private actions');
});
it('anonymous mode lock reason distinguishes gate and DM posture', () => {
const reason = getMeshTerminalWriteLockReason({
wormholeRequired: true,
wormholeReady: true,
anonymousMode: true,
anonymousModeReady: true,
});
expect(reason).toContain('gate chat (transitional lane)');
expect(reason).toContain('Dead Drop (stronger private lane)');
expect(reason).not.toContain('hardened');
});
it('keeps read-only terminal commands available', () => {
expect(isMeshTerminalWriteCommand('status', [])).toBe(false);
expect(isMeshTerminalWriteCommand('signals', ['10'])).toBe(false);
expect(isMeshTerminalWriteCommand('mesh', ['listen', '20'])).toBe(false);
expect(isMeshTerminalWriteCommand('messages', [])).toBe(false);
});
it('MeshTerminal does not use raw agent-id fetch as the ordinary DM send path', () => {
const terminal = fs.readFileSync(
path.resolve(__dirname, '../../components/MeshTerminal.tsx'),
'utf-8',
);
expect(terminal).toContain('fetchDmPublicKey');
expect(terminal).toContain("only for legacy migration");
expect(terminal).not.toContain('/api/mesh/dm/pubkey?agent_id=');
});
it('MeshTerminal inbox surface owns mailbox refresh instead of racing the unread poll loop', () => {
const terminal = fs.readFileSync(
path.resolve(__dirname, '../../components/MeshTerminal.tsx'),
'utf-8',
);
expect(terminal).toContain("if (!isOpen || !nodeIdentity || !hasSovereignty() || !getDMNotify() || surfacePanel === 'inbox') return;");
expect(terminal).toContain('classifyTick(hasMore, catchUpBudget, 15_000)');
expect(terminal).toContain('() => void loadInboxSurface(classification.refreshCount)');
});
});
@@ -0,0 +1,569 @@
import '@testing-library/jest-dom/vitest';
import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
let contactsState: Record<string, any> = {};
const mocks = vi.hoisted(() => ({
buildMailboxClaims: vi.fn(async () => []),
countDmMailboxes: vi.fn(async () => ({ ok: true, count: 0 })),
ensureRegisteredDmKey: vi.fn(async () => ({ dhPubKey: 'local-dh', dhAlgo: 'X25519' })),
fetchDmPublicKey: vi.fn(async () => ({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' })),
pollDmMailboxes: vi.fn(async () => ({ ok: true, messages: [] })),
sendDmMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })),
sendOffLedgerConsentMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })),
sharedMailboxToken: vi.fn(async () => 'shared-token'),
buildContactAcceptMessage: vi.fn(() => 'accept'),
buildContactDenyMessage: vi.fn(() => 'deny'),
buildContactOfferMessage: vi.fn(() => 'offer'),
generateSharedAlias: vi.fn(() => 'alias-123'),
mergeAliasHistory: vi.fn((history?: string[]) => history || []),
parseAliasRotateMessage: vi.fn(() => null),
parseDmConsentMessage: vi.fn(() => null),
preferredDmPeerId: vi.fn((peerId: string) => peerId),
allDmPeerIds: vi.fn(() => []),
purgeBrowserDmState: vi.fn(async () => {}),
ratchetDecryptDM: vi.fn(async () => {
throw new Error('no_ratchet_state');
}),
ratchetEncryptDM: vi.fn(async () => 'ratchet-ciphertext'),
addContact: vi.fn(),
blockContact: vi.fn(),
decryptDM: vi.fn(async () => 'plaintext'),
decryptSenderSealPayloadLocally: vi.fn(async () => ''),
deriveSharedKey: vi.fn(async () => ({})),
encryptDM: vi.fn(async () => 'ciphertext'),
getContacts: vi.fn(() => contactsState),
getDHAlgo: vi.fn(() => 'X25519'),
getNodeIdentity: vi.fn(() => ({
nodeId: '!sb_local',
publicKey: 'local-pub',
privateKey: 'local-priv',
})),
hasSovereignty: vi.fn(() => true),
hydrateWormholeContacts: vi.fn(async () => contactsState),
purgeBrowserContactGraph: vi.fn(),
purgeBrowserSigningMaterial: vi.fn(),
removeContact: vi.fn(),
unblockContact: vi.fn(),
unwrapSenderSealPayload: vi.fn(() => ({ version: 'v2', ephemeralPub: '' })),
updateContact: vi.fn(),
verifyNodeIdBindingFromPublicKey: vi.fn(async () => true),
verifyRawSignature: vi.fn(async () => true),
getSenderRecoveryState: vi.fn(() => 'verified'),
recoverSenderSealWithFallback: vi.fn(async () => null),
requiresSenderRecovery: vi.fn(() => false),
shouldKeepUnresolvedRequestVisible: vi.fn(() => false),
shouldPromoteRecoveredSenderForBootstrap: vi.fn(() => false),
shouldPromoteRecoveredSenderForKnownContact: vi.fn(() => false),
bootstrapDecryptAccessRequest: vi.fn(async () => 'offer'),
bootstrapEncryptAccessRequest: vi.fn(async () => 'x3dh1:bootstrap'),
canUseWormholeBootstrap: vi.fn(async () => false),
fetchWormholeStatus: vi.fn(async () => ({ ready: true, transport_tier: 'private_strong' })),
fetchWormholeIdentity: vi.fn(async () => ({ node_id: '!sb_local', public_key: 'local-pub' })),
prepareWormholeInteractiveLane: vi.fn(async () => ({
ready: true,
settingsEnabled: true,
transportTier: 'private_transitional',
identity: { node_id: '!sb_local', public_key: 'local-pub' },
})),
importWormholeDmInvite: vi.fn(async () => ({
ok: true,
peer_id: '!sb_imported',
trust_fingerprint: 'invitefp',
trust_level: 'invite_pinned',
})),
isWormholeReady: vi.fn(async () => true),
isWormholeSecureRequired: vi.fn(async () => false),
issueWormholePairwiseAlias: vi.fn(async () => ({ ok: true, shared_alias: 'alias-123' })),
openWormholeSenderSeal: vi.fn(async () => ({ sender_id: '!sb_peer', seal_verified: true })),
}));
vi.mock('@/lib/api', () => ({
API_BASE: 'http://localhost:8000',
}));
vi.mock('@/mesh/meshDmClient', () => ({
buildMailboxClaims: mocks.buildMailboxClaims,
countDmMailboxes: mocks.countDmMailboxes,
ensureRegisteredDmKey: mocks.ensureRegisteredDmKey,
fetchDmPublicKey: mocks.fetchDmPublicKey,
pollDmMailboxes: mocks.pollDmMailboxes,
sendDmMessage: mocks.sendDmMessage,
sendOffLedgerConsentMessage: mocks.sendOffLedgerConsentMessage,
sharedMailboxToken: mocks.sharedMailboxToken,
}));
vi.mock('@/mesh/meshDmConsent', () => ({
allDmPeerIds: mocks.allDmPeerIds,
buildContactAcceptMessage: mocks.buildContactAcceptMessage,
buildContactDenyMessage: mocks.buildContactDenyMessage,
buildContactOfferMessage: mocks.buildContactOfferMessage,
generateSharedAlias: mocks.generateSharedAlias,
mergeAliasHistory: mocks.mergeAliasHistory,
parseAliasRotateMessage: mocks.parseAliasRotateMessage,
parseDmConsentMessage: mocks.parseDmConsentMessage,
preferredDmPeerId: mocks.preferredDmPeerId,
}));
vi.mock('@/mesh/meshDmWorkerClient', () => ({
purgeBrowserDmState: mocks.purgeBrowserDmState,
ratchetDecryptDM: mocks.ratchetDecryptDM,
ratchetEncryptDM: mocks.ratchetEncryptDM,
}));
vi.mock('@/mesh/meshIdentity', () => ({
addContact: mocks.addContact,
blockContact: mocks.blockContact,
decryptDM: mocks.decryptDM,
decryptSenderSealPayloadLocally: mocks.decryptSenderSealPayloadLocally,
deriveSharedKey: mocks.deriveSharedKey,
encryptDM: mocks.encryptDM,
getContacts: mocks.getContacts,
getDHAlgo: mocks.getDHAlgo,
getNodeIdentity: mocks.getNodeIdentity,
hasSovereignty: mocks.hasSovereignty,
hydrateWormholeContacts: mocks.hydrateWormholeContacts,
purgeBrowserContactGraph: mocks.purgeBrowserContactGraph,
purgeBrowserSigningMaterial: mocks.purgeBrowserSigningMaterial,
removeContact: mocks.removeContact,
unblockContact: mocks.unblockContact,
unwrapSenderSealPayload: mocks.unwrapSenderSealPayload,
updateContact: mocks.updateContact,
verifyNodeIdBindingFromPublicKey: mocks.verifyNodeIdBindingFromPublicKey,
verifyRawSignature: mocks.verifyRawSignature,
}));
vi.mock('@/mesh/requestSenderRecovery', () => ({
getSenderRecoveryState: mocks.getSenderRecoveryState,
recoverSenderSealWithFallback: mocks.recoverSenderSealWithFallback,
requiresSenderRecovery: mocks.requiresSenderRecovery,
shouldKeepUnresolvedRequestVisible: mocks.shouldKeepUnresolvedRequestVisible,
shouldPromoteRecoveredSenderForBootstrap: mocks.shouldPromoteRecoveredSenderForBootstrap,
shouldPromoteRecoveredSenderForKnownContact: mocks.shouldPromoteRecoveredSenderForKnownContact,
}));
vi.mock('@/mesh/wormholeDmBootstrapClient', () => ({
bootstrapDecryptAccessRequest: mocks.bootstrapDecryptAccessRequest,
bootstrapEncryptAccessRequest: mocks.bootstrapEncryptAccessRequest,
canUseWormholeBootstrap: mocks.canUseWormholeBootstrap,
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
fetchWormholeStatus: mocks.fetchWormholeStatus,
fetchWormholeIdentity: mocks.fetchWormholeIdentity,
prepareWormholeInteractiveLane: mocks.prepareWormholeInteractiveLane,
getWormholeDmInviteImportErrorResult: (error: unknown) =>
error && typeof error === 'object' && 'result' in (error as Record<string, unknown>)
? (((error as Record<string, unknown>).result as Record<string, unknown>) || null)
: null,
importWormholeDmInvite: mocks.importWormholeDmInvite,
isWormholeReady: mocks.isWormholeReady,
isWormholeSecureRequired: mocks.isWormholeSecureRequired,
issueWormholePairwiseAlias: mocks.issueWormholePairwiseAlias,
openWormholeSenderSeal: mocks.openWormholeSenderSeal,
}));
import MessagesView from '@/components/InfonetTerminal/MessagesView';
function renderMessagesView(options?: {
onOpenDeadDrop?: (peerId: string, opts?: { showSas?: boolean }) => void;
}) {
return render(<MessagesView onBack={() => {}} onOpenDeadDrop={options?.onOpenDeadDrop} />);
}
async function openComposeForRecipient(recipient: string, body: string) {
fireEvent.click(screen.getByRole('button', { name: 'COMPOSE' }));
fireEvent.change(screen.getByLabelText(/Recipient agent ID/i), {
target: { value: recipient },
});
fireEvent.change(screen.getByLabelText(/Message/i), {
target: { value: body },
});
await screen.findByLabelText(/Recipient agent ID/i);
}
describe('MessagesView first-contact trust UX', () => {
beforeEach(() => {
cleanup();
localStorage.clear();
contactsState = {};
vi.clearAllMocks();
mocks.getContacts.mockImplementation(() => contactsState);
mocks.hydrateWormholeContacts.mockImplementation(async () => contactsState);
mocks.fetchWormholeStatus.mockResolvedValue({ ready: true, transport_tier: 'private_strong' });
mocks.prepareWormholeInteractiveLane.mockResolvedValue({
ready: true,
settingsEnabled: true,
transportTier: 'private_transitional',
identity: { node_id: '!sb_local', public_key: 'local-pub' },
});
mocks.isWormholeSecureRequired.mockResolvedValue(false);
mocks.getNodeIdentity.mockReturnValue({
nodeId: '!sb_local',
publicKey: 'local-pub',
privateKey: 'local-priv',
});
mocks.hasSovereignty.mockReturnValue(true);
mocks.buildMailboxClaims.mockResolvedValue([]);
mocks.pollDmMailboxes.mockResolvedValue({ ok: true, messages: [] });
mocks.countDmMailboxes.mockResolvedValue({ ok: true, count: 0 });
mocks.ensureRegisteredDmKey.mockResolvedValue({ dhPubKey: 'local-dh', dhAlgo: 'X25519' });
mocks.fetchDmPublicKey.mockResolvedValue({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' });
mocks.sendOffLedgerConsentMessage.mockResolvedValue({ ok: true, transport: 'relay' });
mocks.canUseWormholeBootstrap.mockResolvedValue(false);
});
afterEach(() => {
cleanup();
});
it('blocks unknown first contact until a signed invite is imported', async () => {
renderMessagesView();
await openComposeForRecipient('!sb_unknown', 'hello from first contact');
expect(await screen.findByText('Verified First Contact Required')).toBeInTheDocument();
expect(
screen.getByText(/Secure request bootstrap is blocked until you import a signed invite/i),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeDisabled();
});
it('can jump directly from the downgrade warning into invite import flow', async () => {
renderMessagesView();
await openComposeForRecipient('!sb_unknown', 'hello from first contact');
fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' }));
expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument();
expect(screen.getByLabelText(/Local Alias/i)).toHaveValue('!sb_unknown');
});
it('does not expose a TOFU downgrade button for first contact anymore', async () => {
renderMessagesView();
await openComposeForRecipient('!sb_unknown', 'hello from first contact');
expect(screen.queryByRole('button', { name: /Explicitly Allow TOFU/i })).not.toBeInTheDocument();
expect(mocks.sendOffLedgerConsentMessage).not.toHaveBeenCalled();
});
it('does not require the TOFU override when the contact is invite-pinned already', async () => {
contactsState = {
'!sb_invited': {
alias: 'Pinned Peer',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedTrustFingerprint: 'abcdef123456',
invitePinnedRootFingerprint: 'rootabcdef123456',
invitePinnedRootManifestFingerprint: 'manifestabcdef123456',
invitePinnedRootWitnessPolicyFingerprint: 'policyabcdef123456',
invitePinnedRootWitnessThreshold: 2,
invitePinnedRootWitnessCount: 3,
invitePinnedRootManifestGeneration: 1,
invitePinnedRootRotationProven: true,
invitePinnedAt: 123,
remotePrekeyFingerprint: 'abcdef123456',
remotePrekeyRootFingerprint: 'rootabcdef123456',
remotePrekeyRootManifestFingerprint: 'manifestabcdef123456',
remotePrekeyRootWitnessPolicyFingerprint: 'policyabcdef123456',
remotePrekeyRootWitnessThreshold: 2,
remotePrekeyRootWitnessCount: 3,
remotePrekeyRootManifestGeneration: 1,
remotePrekeyRootRotationProven: true,
},
};
renderMessagesView();
await openComposeForRecipient('!sb_invited', 'hello to pinned peer');
expect(screen.queryByText('Unverified First Contact')).not.toBeInTheDocument();
expect(await screen.findByText('ROOT LOCAL QUORUM')).toBeInTheDocument();
expect(await screen.findByText(/Local quorum root rootabcd\.\.123456/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeEnabled();
});
it('warms the private lane in the background before sending secure mail', async () => {
contactsState = {
'!sb_pinned': {
alias: 'Pinned Peer',
blocked: false,
trust_level: 'invite_pinned',
dhPubKey: 'peer-dh',
remotePrekeyFingerprint: 'abcdef123456',
},
};
mocks.fetchWormholeStatus.mockResolvedValue({ ready: false, transport_tier: 'public_degraded' });
renderMessagesView();
await openComposeForRecipient('!sb_pinned', 'hello after warmup');
fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' }));
await screen.findByText(/Mail delivered to Pinned Peer/i);
expect(mocks.prepareWormholeInteractiveLane).toHaveBeenCalled();
expect(mocks.sendDmMessage).toHaveBeenCalled();
});
it('does not flatten witness policy not met into a generic witnessed root label', async () => {
contactsState = {
'!sb_policy': {
alias: 'Policy Peer',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedTrustFingerprint: 'policyfingerprint123456',
invitePinnedRootFingerprint: 'rootpolicyabcdef123456',
invitePinnedRootManifestFingerprint: 'manifestpolicyabcdef123456',
invitePinnedRootWitnessPolicyFingerprint: 'policyabcdef123456',
invitePinnedRootWitnessThreshold: 2,
invitePinnedRootWitnessCount: 1,
invitePinnedRootManifestGeneration: 1,
invitePinnedRootRotationProven: true,
invitePinnedAt: 123,
remotePrekeyFingerprint: 'policyfingerprint123456',
remotePrekeyRootFingerprint: 'rootpolicyabcdef123456',
remotePrekeyRootManifestFingerprint: 'manifestpolicyabcdef123456',
remotePrekeyRootWitnessPolicyFingerprint: 'policyabcdef123456',
remotePrekeyRootWitnessThreshold: 2,
remotePrekeyRootWitnessCount: 1,
remotePrekeyRootManifestGeneration: 1,
remotePrekeyRootRotationProven: true,
},
};
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText(/Witness-policy root rootpoli\.\.123456/i)).toBeInTheDocument();
expect(screen.queryByText(/Witnessed root rootpoli\.\.123456/i)).not.toBeInTheDocument();
});
it('shows an import-invite shortcut for unpinned contacts in the contact list', async () => {
contactsState = {
'!sb_unpinned': {
alias: 'Weak Peer',
blocked: false,
dhPubKey: 'peer-dh',
trust_level: 'unpinned',
},
};
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
const importButton = await screen.findByRole('button', { name: 'Import Invite' });
fireEvent.click(importButton);
expect(screen.getByLabelText(/Local Alias/i)).toHaveValue('!sb_unpinned');
});
it('routes continuity reverify from Secure Messages into Dead Drop with SAS visible', async () => {
contactsState = {
'!sb_reverify': {
alias: 'Broken Root Peer',
blocked: false,
trust_level: 'continuity_broken',
remotePrekeyObservedFingerprint: 'observed123456',
remotePrekeyObservedRootFingerprint: 'rootobserved123456',
remotePrekeyRootMismatch: true,
},
};
const onOpenDeadDrop = vi.fn();
renderMessagesView({ onOpenDeadDrop });
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
const reverifyButton = await screen.findByRole('button', { name: 'REVERIFY NOW' });
fireEvent.click(reverifyButton);
expect(onOpenDeadDrop).toHaveBeenCalledWith('!sb_reverify', { showSas: true });
});
it('still blocks first contact when legacy verified flags and a dh key are seeded on an unpinned contact', async () => {
contactsState = {
'!sb_seeded': {
alias: 'Seeded Peer',
blocked: false,
dhPubKey: 'forged-dh',
verify_inband: true,
verify_registry: true,
verified: true,
trust_level: 'unpinned',
trustSummary: {
state: 'unpinned',
label: 'UNVERIFIED',
severity: 'warn',
detail: 'invite required',
verifiedFirstContact: false,
recommendedAction: 'import_invite',
legacyLookup: false,
inviteAttested: false,
registryMismatch: false,
transparencyConflict: false,
},
},
};
renderMessagesView();
await openComposeForRecipient('!sb_seeded', 'hello from forged first contact');
expect(await screen.findByText('Verified First Contact Required')).toBeInTheDocument();
expect(
screen.getByText(/Secure request bootstrap is blocked until you import a signed invite/i),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeDisabled();
});
it('blocks ambient legacy lookup for verified contacts that still lack an invite handle', async () => {
contactsState = {
'!sb_legacy': {
alias: 'Legacy Peer',
blocked: false,
trust_level: 'sas_verified',
remotePrekeyLookupMode: 'legacy_agent_id',
trustSummary: {
state: 'sas_verified',
label: 'SAS VERIFIED',
severity: 'good',
detail: 'legacy lookup still active',
verifiedFirstContact: true,
recommendedAction: 'import_invite',
legacyLookup: true,
inviteAttested: false,
registryMismatch: false,
transparencyConflict: false,
},
},
};
renderMessagesView();
await openComposeForRecipient('!sb_legacy', 'hello from a legacy lookup contact');
fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' }));
expect(
await screen.findByText(
/Import or re-import a signed invite before sending a contact request; legacy direct lookup is disabled\./i,
),
).toBeInTheDocument();
expect(mocks.fetchDmPublicKey).not.toHaveBeenCalled();
});
it('announces attested invite imports as INVITE PINNED', async () => {
mocks.importWormholeDmInvite.mockResolvedValueOnce({
ok: true,
peer_id: '!sb_attested',
trust_fingerprint: 'invitefp-attested',
trust_level: 'invite_pinned',
contact: {},
});
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' }));
expect(
await screen.findByText(/INVITE PINNED for !sb_attested \(invitefp\.\.tested\)\./i),
).toBeInTheDocument();
});
it('announces compat invite imports as TOFU PINNED with backend detail', async () => {
mocks.importWormholeDmInvite.mockResolvedValueOnce({
ok: true,
peer_id: '!sb_compat',
trust_fingerprint: 'invitefp-compat',
trust_level: 'tofu_pinned',
detail: 'legacy invite imported as tofu_pinned; SAS verification required before first contact',
contact: {},
});
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' }));
expect(
await screen.findByText(/TOFU PINNED for !sb_compat \(invitefp\.\.compat\)\./i),
).toBeInTheDocument();
expect(
screen.getByText(/legacy invite imported as tofu_pinned; SAS verification required before first contact/i),
).toBeInTheDocument();
});
it('surfaces stable root continuity breaks on invite re-import', async () => {
contactsState = {
'!sb_attested': {
alias: 'Pinned Peer',
blocked: false,
trust_level: 'continuity_broken',
invitePinnedTrustFingerprint: 'oldfingerprint123456',
invitePinnedRootFingerprint: 'rootold123456',
remotePrekeyFingerprint: 'newfingerprint654321',
remotePrekeyObservedFingerprint: 'newfingerprint654321',
remotePrekeyRootFingerprint: 'rootold123456',
remotePrekeyObservedRootFingerprint: 'rootnew654321',
remotePrekeyRootMismatch: true,
},
};
const error = Object.assign(
new Error(
'signed invite root continuity mismatch; re-verify SAS or replace the signed invite before trusting this root change',
),
{
result: {
ok: false,
peer_id: '!sb_attested',
trust_level: 'continuity_broken',
detail:
'signed invite root continuity mismatch; re-verify SAS or replace the signed invite before trusting this root change',
contact: {},
},
},
);
mocks.importWormholeDmInvite.mockRejectedValueOnce(error);
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' }));
expect(
await screen.findByText(/CONTINUITY BROKEN for Pinned Peer\. Stable root continuity changed\./i),
).toBeInTheDocument();
expect(
screen.getByText(/re-verify SAS in Dead Drop or replace the signed invite before trusting this contact again/i),
).toBeInTheDocument();
});
it('uses non-blocking secure-mail startup language while the DM lane warms', async () => {
mocks.fetchWormholeStatus.mockResolvedValueOnce({ ready: false, transport_tier: 'public_degraded' });
mocks.prepareWormholeInteractiveLane.mockImplementation(
() =>
new Promise(() => {
/* keep background warm-up pending for this assertion */
}),
);
renderMessagesView();
expect(
await screen.findByText(/Preparing secure mail in the background/i),
).toBeInTheDocument();
expect(screen.queryByText(/LOCKED/i)).not.toBeInTheDocument();
expect(screen.queryByText(/enter the Wormhole/i)).not.toBeInTheDocument();
});
});
@@ -0,0 +1,148 @@
import React from 'react';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const deferred = <T,>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
const fetchWormholeStatus = vi.fn(async () => ({
ready: false,
running: false,
transport_tier: 'public_degraded',
transport_active: 'public_degraded',
}));
const prepareWormholeInteractiveLane = vi.fn();
const fetchWormholeSettings = vi.fn(async () => ({
enabled: false,
anonymous_mode: false,
}));
const purgeBrowserContactGraph = vi.fn();
const purgeBrowserSigningMaterial = vi.fn();
const setSecureModeCached = vi.fn();
const getNodeIdentity = vi.fn(() => null);
const generateNodeKeys = vi.fn(async () => ({}));
const purgeBrowserDmState = vi.fn(async () => {});
const fetchInfonetNodeStatusSnapshot = vi.fn(async () => ({
enabled: false,
peers_ready: false,
identity_ready: false,
}));
const requestMeshTerminalOpen = vi.fn();
const subscribeSecureMeshTerminalLauncherOpen = vi.fn(() => () => {});
const classifyUpdateRuntime = vi.fn(() => ({
action: 'auto_apply',
detail: 'test',
}));
const getDesktopUpdateContext = vi.fn(() => ({
packaged: false,
ownsLocalBackend: false,
}));
const getPreferredManualUpdateUrl = vi.fn(() => 'https://example.test/releases/latest');
const getUpdateAction = vi.fn(() => 'auto_apply');
const controlPlaneFetch = vi.fn();
vi.mock('@/mesh/wormholeIdentityClient', () => ({
fetchWormholeStatus,
prepareWormholeInteractiveLane,
}));
vi.mock('@/mesh/wormholeClient', () => ({
fetchWormholeSettings,
}));
vi.mock('@/mesh/meshIdentity', () => ({
purgeBrowserContactGraph,
purgeBrowserSigningMaterial,
setSecureModeCached,
getNodeIdentity,
generateNodeKeys,
}));
vi.mock('@/mesh/meshDmWorkerClient', () => ({
purgeBrowserDmState,
}));
vi.mock('@/mesh/controlPlaneStatusClient', () => ({
fetchInfonetNodeStatusSnapshot,
}));
vi.mock('@/lib/meshTerminalLauncher', () => ({
requestMeshTerminalOpen,
subscribeSecureMeshTerminalLauncherOpen,
}));
vi.mock('@/lib/updateRuntime', () => ({
classifyUpdateRuntime,
getDesktopUpdateContext,
getPreferredManualUpdateUrl,
getUpdateAction,
}));
vi.mock('@/lib/controlPlane', () => ({
controlPlaneFetch,
}));
describe('TopRightControls terminal launcher', () => {
beforeEach(() => {
vi.clearAllMocks();
fetchWormholeStatus.mockResolvedValue({
ready: false,
running: false,
transport_tier: 'public_degraded',
transport_active: 'public_degraded',
});
fetchWormholeSettings.mockResolvedValue({
enabled: false,
anonymous_mode: false,
});
fetchInfonetNodeStatusSnapshot.mockResolvedValue({
enabled: false,
peers_ready: false,
identity_ready: false,
});
});
afterEach(() => {
cleanup();
});
it('opens the terminal immediately while Wormhole prep continues in the background', async () => {
const prep = deferred<{
ready: boolean;
settingsEnabled: boolean;
transportTier: string;
identity: null;
}>();
prepareWormholeInteractiveLane.mockReturnValue(prep.promise);
const { default: TopRightControls } = await import('@/components/TopRightControls');
const onTerminalToggle = vi.fn();
render(<TopRightControls onTerminalToggle={onTerminalToggle} />);
fireEvent.click(await screen.findByRole('button', { name: /terminal/i }));
expect(await screen.findByRole('button', { name: /activate wormhole/i })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /activate wormhole/i }));
await waitFor(() => expect(onTerminalToggle).toHaveBeenCalledTimes(1));
await waitFor(() =>
expect(screen.queryByRole('button', { name: /activate wormhole/i })).toBeNull(),
);
expect(prepareWormholeInteractiveLane).toHaveBeenCalledWith({ bootstrapIdentity: true });
prep.resolve({
ready: true,
settingsEnabled: true,
transportTier: 'private_control_only',
identity: null,
});
});
});
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest';
import {
formatLegacyCompatibilitySeenAt,
hasLegacyCompatibilityActivity,
summarizeLegacyCompatibility,
type LegacyCompatibilitySnapshot,
} from '@/mesh/wormholeCompatibility';
describe('wormholeCompatibility helpers', () => {
it('summarizes empty snapshots with zeroed metrics', () => {
const items = summarizeLegacyCompatibility(undefined);
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({
key: 'legacy_node_id_binding',
blocked: false,
count: 0,
blockedCount: 0,
targetVersion: 'n/a',
targetDate: 'n/a',
recentTargets: [],
});
expect(items[1]).toMatchObject({
key: 'legacy_agent_id_lookup',
blocked: false,
count: 0,
blockedCount: 0,
targetVersion: 'n/a',
targetDate: 'n/a',
recentTargets: [],
});
expect(hasLegacyCompatibilityActivity(undefined)).toBe(false);
});
it('formats legacy usage, block state, and recent targets', () => {
const snapshot: LegacyCompatibilitySnapshot = {
sunset: {
legacy_node_id_binding: {
target_version: '0.10.0',
target_date: '2026-06-01',
blocked: true,
},
legacy_agent_id_lookup: {
target_version: '0.10.0',
target_date: '2026-06-01',
blocked: false,
},
},
usage: {
legacy_node_id_binding: {
count: 4,
blocked_count: 2,
last_seen_at: 1712345678,
recent_targets: [
{
node_id: 'abcdef0123456789',
current_node_id: 'fedcba9876543210abcdef0123456789',
},
],
},
legacy_agent_id_lookup: {
count: 3,
blocked_count: 1,
last_seen_at: 1712345000,
recent_targets: [
{
agent_id: 'agent-xyz-0123456789',
lookup_kinds: ['prekey_bundle', 'dh_pubkey'],
},
],
},
},
};
const items = summarizeLegacyCompatibility(snapshot);
expect(items[0]).toMatchObject({
blocked: true,
count: 4,
blockedCount: 2,
targetVersion: '0.10.0',
targetDate: '2026-06-01',
});
expect(items[0].recentTargets[0]).toContain('abcdef0123...');
expect(items[0].recentTargets[0]).toContain('fedcba9876...');
expect(items[1]).toMatchObject({
blocked: false,
count: 3,
blockedCount: 1,
targetVersion: '0.10.0',
targetDate: '2026-06-01',
});
expect(items[1].recentTargets[0]).toContain('agent-xyz-...');
expect(items[1].recentTargets[0]).toContain('prekey_bundle, dh_pubkey');
expect(hasLegacyCompatibilityActivity(snapshot)).toBe(true);
});
it('formats seen timestamps as stable UTC text', () => {
expect(formatLegacyCompatibilitySeenAt(0)).toBe('never');
expect(formatLegacyCompatibilitySeenAt(1712345678)).toBe('2024-04-05 19:34Z');
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
/**
* Sprint 4D behavioral tests — page.tsx wormhole teardown and layer sync.
*
* These tests exercise actual runtime logic:
* 1. teardownWormholeOnClose — calls leaveWormhole only when state is ready or running
* 2. Layer sync first-mount suppression — initial sync does NOT dispatch LAYER_TOGGLE_EVENT
*/
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { teardownWormholeOnClose } from '@/lib/wormholeTeardown';
import { LAYER_TOGGLE_EVENT } from '@/hooks/useDataPolling';
// ─── teardownWormholeOnClose ──────────────────────────────────────────────
describe('page.tsx behavior — teardownWormholeOnClose', () => {
let fetchState: ReturnType<typeof vi.fn>;
let leave: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchState = vi.fn();
leave = vi.fn().mockResolvedValue({});
});
it('calls leaveWormhole when state is ready', async () => {
fetchState.mockResolvedValue({ ready: true, running: false });
await teardownWormholeOnClose(fetchState, leave);
expect(fetchState).toHaveBeenCalledWith(false);
expect(leave).toHaveBeenCalledTimes(1);
});
it('calls leaveWormhole when state is running', async () => {
fetchState.mockResolvedValue({ ready: false, running: true });
await teardownWormholeOnClose(fetchState, leave);
expect(leave).toHaveBeenCalledTimes(1);
});
it('calls leaveWormhole when state is both ready and running', async () => {
fetchState.mockResolvedValue({ ready: true, running: true });
await teardownWormholeOnClose(fetchState, leave);
expect(leave).toHaveBeenCalledTimes(1);
});
it('does NOT call leaveWormhole when state is neither ready nor running', async () => {
fetchState.mockResolvedValue({ ready: false, running: false });
await teardownWormholeOnClose(fetchState, leave);
expect(fetchState).toHaveBeenCalledWith(false);
expect(leave).not.toHaveBeenCalled();
});
it('does NOT call leaveWormhole when state is null', async () => {
fetchState.mockResolvedValue(null);
await teardownWormholeOnClose(fetchState, leave);
expect(leave).not.toHaveBeenCalled();
});
it('swallows fetchState errors gracefully', async () => {
fetchState.mockRejectedValue(new Error('network down'));
await teardownWormholeOnClose(fetchState, leave);
expect(leave).not.toHaveBeenCalled();
// No error thrown — handler is best-effort
});
it('swallows leaveWormhole errors gracefully', async () => {
fetchState.mockResolvedValue({ ready: true });
leave.mockRejectedValue(new Error('leave failed'));
await teardownWormholeOnClose(fetchState, leave);
// No error thrown — handler is best-effort
});
it('always passes force=false to fetchState', async () => {
fetchState.mockResolvedValue({ ready: true });
await teardownWormholeOnClose(fetchState, leave);
expect(fetchState).toHaveBeenCalledWith(false);
expect(fetchState).not.toHaveBeenCalledWith(true);
});
});
// ─── Layer sync first-mount suppression ───────────────────────────────────
describe('page.tsx behavior — layer sync first-mount suppression', () => {
it('LAYER_TOGGLE_EVENT is the expected string constant', () => {
expect(LAYER_TOGGLE_EVENT).toBe('sb:layer-toggle');
});
it('first-mount ref pattern suppresses dispatch, subsequent calls dispatch', () => {
// Simulate the initialLayerSyncRef pattern from page.tsx
const initialSyncDone = { current: false };
const dispatched: boolean[] = [];
const syncLayers = (triggerRefetch: boolean) => {
if (triggerRefetch) {
dispatched.push(true);
} else {
dispatched.push(false);
}
};
// First call (mount): should pass false → no dispatch
if (!initialSyncDone.current) {
initialSyncDone.current = true;
syncLayers(false);
} else {
syncLayers(true);
}
expect(dispatched).toEqual([false]);
// Second call (layer change): should pass true → dispatch
if (!initialSyncDone.current) {
initialSyncDone.current = true;
syncLayers(false);
} else {
syncLayers(true);
}
expect(dispatched).toEqual([false, true]);
// Third call (another layer change): should still dispatch
if (!initialSyncDone.current) {
initialSyncDone.current = true;
syncLayers(false);
} else {
syncLayers(true);
}
expect(dispatched).toEqual([false, true, true]);
});
it('page.tsx uses initialLayerSyncRef for first-mount suppression', () => {
const fs = require('fs');
const path = require('path');
const page = fs.readFileSync(
path.resolve(__dirname, '../../app/page.tsx'),
'utf-8',
);
expect(page).toContain('initialLayerSyncRef');
expect(page).toContain('void syncLayers(false)');
expect(page).toContain('void syncLayers(true)');
});
});
@@ -0,0 +1,235 @@
/**
* Sprint 4B regression tests — page.tsx decomposition boundary checks.
*
* These tests validate the frozen contract for page.tsx decomposition:
* 1. InfonetTerminal onClose still calls leaveWormhole when wormhole is ready/running
* 2. Initial /api/layers sync does NOT dispatch LAYER_TOGGLE_EVENT on first mount
* 3. launchMeshChatTab preserves atomic leftOpen + leftMeshExpanded + meshChatLaunchRequest
* 4. LocateBar extracted to page-local module
* 5. SentinelInfoModal extracted to page-local module
* 6. page.tsx retains all frozen-contract orchestration items
* 7. MeshChat and MaplibreViewer integration boundaries remain intact
* 8. No admin-session or proxy regression introduced
*/
import { describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
const APP_DIR = path.resolve(__dirname, '../../app');
function readAppFile(name: string): string {
return fs.readFileSync(path.join(APP_DIR, name), 'utf-8');
}
// ─── Extraction verification ────────────────────────────────────────────────
describe('page.tsx decomposition — extraction targets', () => {
it('LocateBar is defined in its own page-local module', () => {
const locateBar = readAppFile('LocateBar.tsx');
expect(locateBar).toMatch(/export\s+function\s+LocateBar/);
expect(locateBar).toContain('onLocate');
expect(locateBar).toContain('onOpenChange');
});
it('SentinelInfoModal is defined in its own page-local module', () => {
const modal = readAppFile('SentinelInfoModal.tsx');
expect(modal).toMatch(/export\s+function\s+SentinelInfoModal/);
expect(modal).toContain('onClose');
expect(modal).toContain('SENTINEL HUB IMAGERY');
});
it('page.tsx imports LocateBar from page-local module', () => {
const page = readAppFile('page.tsx');
expect(page).toMatch(/import\s*\{.*LocateBar.*\}\s*from\s+['"]\.\/LocateBar['"]/);
});
it('page.tsx imports SentinelInfoModal from page-local module', () => {
const page = readAppFile('page.tsx');
expect(page).toMatch(/import\s*\{.*SentinelInfoModal.*\}\s*from\s+['"]\.\/SentinelInfoModal['"]/);
});
it('page.tsx no longer defines LocateBar inline', () => {
const page = readAppFile('page.tsx');
// Should not have the old inline function definition
expect(page).not.toMatch(/^function\s+LocateBar\s*\(/m);
});
});
// ─── InfonetTerminal onClose wormhole teardown ──────────────────────────────
describe('page.tsx decomposition — InfonetTerminal onClose wormhole teardown', () => {
const page = readAppFile('page.tsx');
it('InfonetTerminal onClose delegates to teardownWormholeOnClose', () => {
const infonetSection = page.slice(
page.indexOf('<InfonetTerminal'),
page.indexOf('</InfonetTerminal>') !== -1
? page.indexOf('</InfonetTerminal>')
: page.indexOf('/>', page.indexOf('<InfonetTerminal')) + 2,
);
expect(infonetSection).toContain('teardownWormholeOnClose');
expect(infonetSection).toContain('fetchWormholeState');
expect(infonetSection).toContain('leaveWormhole');
});
it('page.tsx imports teardownWormholeOnClose from wormholeTeardown', () => {
expect(page).toMatch(
/import\s*\{[^}]*teardownWormholeOnClose[^}]*\}\s*from\s+['"]@\/lib\/wormholeTeardown['"]/,
);
});
it('page.tsx imports leaveWormhole and fetchWormholeState from wormholeClient', () => {
expect(page).toMatch(
/import\s*\{[^}]*leaveWormhole[^}]*\}\s*from\s+['"]@\/mesh\/wormholeClient['"]/,
);
expect(page).toMatch(
/import\s*\{[^}]*fetchWormholeState[^}]*\}\s*from\s+['"]@\/mesh\/wormholeClient['"]/,
);
});
});
// ─── /api/layers sync: first mount vs later changes ─────────────────────────
describe('page.tsx decomposition — /api/layers sync behavior', () => {
const page = readAppFile('page.tsx');
it('uses initialLayerSyncRef to distinguish first sync from later changes', () => {
expect(page).toContain('initialLayerSyncRef');
// Check that initialLayerSyncRef is created as a ref
expect(page).toMatch(/initialLayerSyncRef\s*=\s*useRef\s*\(\s*false\s*\)/);
});
it('first mount sync passes false to triggerRefetch (no LAYER_TOGGLE_EVENT)', () => {
// The code checks if initialLayerSyncRef.current is false, then calls syncLayers(false)
expect(page).toMatch(/if\s*\(\s*!initialLayerSyncRef\.current\s*\)/);
// After the check, it sets the ref to true and calls with false
expect(page).toContain('syncLayers(false)');
});
it('subsequent changes dispatch LAYER_TOGGLE_EVENT via syncLayers(true)', () => {
expect(page).toContain('syncLayers(true)');
});
it('LAYER_TOGGLE_EVENT is imported and dispatched inside syncLayers when triggerRefetch=true', () => {
expect(page).toMatch(/import\s*\{[^}]*LAYER_TOGGLE_EVENT[^}]*\}/);
expect(page).toMatch(/LAYER_TOGGLE_EVENT/);
// dispatched conditionally on triggerRefetch
expect(page).toMatch(/if\s*\(\s*triggerRefetch\s*\)/);
expect(page).toContain('new Event(LAYER_TOGGLE_EVENT)');
});
it('activeLayers state is defined in page.tsx (not moved to hook/context)', () => {
expect(page).toMatch(/\[activeLayers,\s*setActiveLayers\]\s*=\s*useState/);
});
});
// ─── launchMeshChatTab atomic update ────────────────────────────────────────
describe('page.tsx decomposition — launchMeshChatTab atomicity', () => {
const page = readAppFile('page.tsx');
it('launchMeshChatTab sets leftOpen to true', () => {
// Extract the launchMeshChatTab definition
const idx = page.indexOf('launchMeshChatTab');
const block = page.slice(idx, idx + 300);
expect(block).toContain('setLeftOpen(true)');
});
it('launchMeshChatTab sets leftMeshExpanded to true', () => {
const idx = page.indexOf('launchMeshChatTab');
const block = page.slice(idx, idx + 300);
expect(block).toContain('setLeftMeshExpanded(true)');
});
it('launchMeshChatTab sets meshChatLaunchRequest with tab, gate, peerId, showSas, and nonce', () => {
const idx = page.indexOf('launchMeshChatTab');
const block = page.slice(idx, idx + 500);
expect(block).toContain('setMeshChatLaunchRequest');
expect(block).toMatch(/tab.*gate.*peerId.*showSas.*nonce|nonce.*Date\.now/);
});
});
// ─── MeshChat and MaplibreViewer integration boundaries ─────────────────────
describe('page.tsx decomposition — child component integration', () => {
const page = readAppFile('page.tsx');
it('MeshChat receives onFlyTo, expanded, onExpandedChange, onSettingsClick, onTerminalToggle, launchRequest props', () => {
const meshChatIdx = page.indexOf('<MeshChat');
const meshChatBlock = page.slice(meshChatIdx, meshChatIdx + 500);
expect(meshChatBlock).toContain('onFlyTo');
expect(meshChatBlock).toContain('expanded=');
expect(meshChatBlock).toContain('onExpandedChange');
expect(meshChatBlock).toContain('onSettingsClick');
expect(meshChatBlock).toContain('onTerminalToggle');
expect(meshChatBlock).toContain('launchRequest');
});
it('MaplibreViewer receives activeLayers and viewBoundsRef props', () => {
const mapIdx = page.indexOf('<MaplibreViewer');
const mapBlock = page.slice(mapIdx, mapIdx + 1500);
expect(mapBlock).toContain('activeLayers');
expect(mapBlock).toContain('viewBoundsRef');
});
it('page.tsx imports MeshChat from @/components/MeshChat', () => {
expect(page).toMatch(/import\s+MeshChat\s+from\s+['"]@\/components\/MeshChat['"]/);
});
it('page.tsx imports MaplibreViewer dynamically', () => {
expect(page).toMatch(/dynamic\s*\(\s*\(\)\s*=>\s*import\s*\(\s*['"]@\/components\/MaplibreViewer['"]\s*\)/);
});
});
// ─── No admin-session or proxy regression ───────────────────────────────────
describe('page.tsx decomposition — no admin-session/proxy regression', () => {
const page = readAppFile('page.tsx');
it('page.tsx still uses useDataPolling at top level', () => {
expect(page).toMatch(/useDataPolling\s*\(\s*\)/);
});
it('page.tsx still uses useBackendStatus', () => {
expect(page).toContain('useBackendStatus');
});
it('page.tsx does not import admin session utilities directly (they stay in hooks)', () => {
// Admin session handling is in useDataPolling and backend hooks, not page.tsx
expect(page).not.toMatch(/adminSession|admin_session/i);
});
it('LocateBar uses backend proxy for geocoding (not direct-only)', () => {
const locateBar = readAppFile('LocateBar.tsx');
expect(locateBar).toContain('API_BASE');
expect(locateBar).toContain('/api/geocode/search');
});
});
// ─── page.tsx retains all frozen-contract orchestration ─────────────────────
describe('page.tsx decomposition — retained orchestration', () => {
const page = readAppFile('page.tsx');
it('page.tsx retains cycleStyle with atomic activeStyle + highres_satellite update', () => {
expect(page).toMatch(/cycleStyle/);
const idx = page.indexOf('cycleStyle');
const block = page.slice(idx, idx + 300);
expect(block).toContain('setActiveStyle');
expect(block).toContain('highres_satellite');
});
it('page.tsx retains viewBoundsRef', () => {
expect(page).toMatch(/viewBoundsRef\s*=\s*useRef/);
});
it('page.tsx retains SSR-safe localStorage hydration', () => {
expect(page).toContain('localStorage.getItem');
expect(page).toContain('sb_left_open');
expect(page).toContain('sb_right_open');
});
it('page.tsx retains infonetOpen state', () => {
expect(page).toMatch(/\[infonetOpen,\s*setInfonetOpen\]\s*=\s*useState/);
});
});
@@ -0,0 +1,370 @@
/**
* Sprint 1C: Proxy admin-key injection coverage tests.
*
* Verifies that the server-side catch-all proxy injects X-Admin-Key on the
* backend leg for routes guarded by require_local_operator:
* - /api/mesh/peers (Sprint 1C addition)
* - /api/tools/* (Sprint 1C addition)
* - /api/wormhole/* (pre-existing, regression)
* - /api/settings/* (pre-existing, regression)
*
* Also verifies that:
* - non-sensitive mesh paths (e.g. mesh/events) do NOT receive injected key
* - browser-supplied x-admin-key is stripped before forwarding (not trusted)
* - no-store cache headers are set on all sensitive paths
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NextRequest } from 'next/server';
import { GET as proxyGet, POST as proxyPost } from '@/app/api/[...path]/route';
import {
POST as postAdminSession,
} from '@/app/api/admin/session/route';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function extractSessionCookie(setCookie: string): string {
return setCookie.split(';')[0] || '';
}
/** Mint a valid admin session and return the raw cookie string. */
async function mintSession(adminKey: string): Promise<string> {
const verifyMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', verifyMock);
const req = new NextRequest('http://localhost/api/admin/session', {
method: 'POST',
body: JSON.stringify({ adminKey }),
headers: { 'Content-Type': 'application/json' },
});
const res = await postAdminSession(req);
return extractSessionCookie(res.headers.get('set-cookie') || '');
}
/** Return the Headers object forwarded to the upstream fetch call. */
function capturedHeaders(fetchMock: ReturnType<typeof vi.fn>): Headers {
const forwarded = fetchMock.mock.calls[0]?.[1];
return new Headers((forwarded as RequestInit | undefined)?.headers);
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
describe('proxy admin-key injection coverage', () => {
const ADMIN_KEY = 'a-valid-admin-key-that-is-at-least-32chars!!';
const originalAdminKey = process.env.ADMIN_KEY;
const originalBackendUrl = process.env.BACKEND_URL;
beforeEach(() => {
process.env.ADMIN_KEY = ADMIN_KEY;
process.env.BACKEND_URL = 'http://127.0.0.1:8000';
vi.restoreAllMocks();
});
afterEach(() => {
process.env.ADMIN_KEY = originalAdminKey;
process.env.BACKEND_URL = originalBackendUrl;
vi.restoreAllMocks();
});
// -------------------------------------------------------------------------
// Sprint 1C: mesh/peers
// -------------------------------------------------------------------------
it('GET /api/mesh/peers with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ peers: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/peers', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['mesh', 'peers'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('POST /api/mesh/peers with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/peers', {
method: 'POST',
body: JSON.stringify({ url: 'http://peer.example.com:8000' }),
headers: { cookie, 'Content-Type': 'application/json' },
});
const res = await proxyPost(req, {
params: Promise.resolve({ path: ['mesh', 'peers'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('GET /api/mesh/peers applies no-store cache headers', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ peers: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/peers', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['mesh', 'peers'] }),
});
expect(res.headers.get('cache-control')).toContain('no-store');
});
// -------------------------------------------------------------------------
// Sprint 1C: tools/*
// -------------------------------------------------------------------------
it('POST /api/tools/shodan/search with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ results: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/tools/shodan/search', {
method: 'POST',
body: JSON.stringify({ query: 'port:22' }),
headers: { cookie, 'Content-Type': 'application/json' },
});
const res = await proxyPost(req, {
params: Promise.resolve({ path: ['tools', 'shodan', 'search'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('GET /api/tools/uw/status with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ configured: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/tools/uw/status', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['tools', 'uw', 'status'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('GET /api/tools/shodan/status applies no-store cache headers', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ configured: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/tools/shodan/status', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['tools', 'shodan', 'status'] }),
});
expect(res.headers.get('cache-control')).toContain('no-store');
});
// -------------------------------------------------------------------------
// Regression: wormhole/* and settings/* unchanged
// -------------------------------------------------------------------------
it('GET /api/wormhole/identity with valid session still injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ identity: null }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/wormhole/identity', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['wormhole', 'identity'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('GET /api/settings/node with valid session still injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ node: {} }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/settings/node', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['settings', 'node'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
// -------------------------------------------------------------------------
// Non-sensitive mesh paths must NOT receive injected admin key
// -------------------------------------------------------------------------
it('GET /api/mesh/events does NOT inject X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response('data: {}\n\n', {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/events', {
method: 'GET',
headers: { cookie },
});
await proxyGet(req, {
params: Promise.resolve({ path: ['mesh', 'events'] }),
});
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBeNull();
});
it('GET /api/mesh/infonet/feed does NOT inject X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ items: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/infonet/feed', {
method: 'GET',
headers: { cookie },
});
await proxyGet(req, {
params: Promise.resolve({ path: ['mesh', 'infonet', 'feed'] }),
});
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBeNull();
});
// -------------------------------------------------------------------------
// Browser-supplied x-admin-key is stripped on all paths
// -------------------------------------------------------------------------
it('browser-supplied x-admin-key is stripped on mesh/peers path', async () => {
process.env.ADMIN_KEY = '';
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ peers: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/mesh/peers', {
method: 'GET',
headers: { 'x-admin-key': 'browser-injected-key' },
});
await proxyGet(req, {
params: Promise.resolve({ path: ['mesh', 'peers'] }),
});
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBeNull();
});
it('browser-supplied x-admin-key is stripped on tools path', async () => {
process.env.ADMIN_KEY = '';
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ configured: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/tools/shodan/status', {
method: 'GET',
headers: { 'x-admin-key': 'browser-injected-key' },
});
await proxyGet(req, {
params: Promise.resolve({ path: ['tools', 'shodan', 'status'] }),
});
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBeNull();
});
});