Features: - Per-Section MAC Exclusions: nft, generator_routes, section.js, i18n - Per-Section Routed DNS: connections, generator_routes, nft, validator, types.ts, section.js, state.uc, reload.uc - Boot Latency Sweep: ui.uc latency boot sweep with fixture dispatchers - WDTT provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - OlcRTC provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - WireGuard-over-TURN tunnel (WDTT) and TCP-over-WebRTC tunnel (OlcRTC) as sidecar processes with local SOCKS5, routed through sing-box outbound Bug fixes: - route.uc: restrict hijack-dns to local DNS inbounds only (dns-in, source-dns-in, dns-health-*) — fixes socket collision between sing-box and dnsmasq on port 53 via SO_REUSEADDR transparent write-back sockets - runtime.uc: add Port 53 socket collision diagnostic with auto-fix (sing-box restart to clear parasitic UNCONN sockets with Recv-Q) - runtime.uc: add dnsmasq dns_redirect check with auto-fix — disables firewall4 DNAT that causes TProxy to rewrite destination to router :53 - failover.uc: add wdtt/olcrtc to failover candidate actions - telegram.uc: show WDTT/OlcRTC installed status in /info Tests: 135/155 shell + 591/591 frontend
182 lines
4.5 KiB
TypeScript
182 lines
4.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
executeShellCommand: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../helpers', () => ({
|
|
executeShellCommand: mocks.executeShellCommand,
|
|
}));
|
|
|
|
import { Tachyon } from '../../types';
|
|
import { store } from '../store.service';
|
|
import {
|
|
getCachedRuntimeUiState,
|
|
refreshRuntimeUiState,
|
|
subscribeRuntimeUiState,
|
|
} from '../runtimeUiState.service';
|
|
|
|
function createUiState(
|
|
status = 'running & enabled',
|
|
running = 1,
|
|
): Tachyon.UiState {
|
|
return {
|
|
service: {
|
|
tachyon: {
|
|
running,
|
|
enabled: 1,
|
|
status,
|
|
dns_configured: running,
|
|
},
|
|
sing_box: {
|
|
running,
|
|
enabled: 0,
|
|
status: running ? 'running but disabled' : 'stopped & disabled',
|
|
},
|
|
},
|
|
capabilities: {
|
|
sing_box_extended: 1,
|
|
sing_box_tiny: 0,
|
|
sing_box_compressed: 0,
|
|
sing_box_lx: 0,
|
|
sing_box_tailscale: 1,
|
|
zapret_installed: 1,
|
|
zapret2_installed: 1,
|
|
byedpi_installed: 0,
|
|
wdtt_installed: 0,
|
|
olcrtc_installed: 0,
|
|
server_inbounds_enabled_count: 0,
|
|
},
|
|
actions: {
|
|
service: [],
|
|
latency: [],
|
|
component: [],
|
|
subscription: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('refreshRuntimeUiState', () => {
|
|
beforeEach(() => {
|
|
store.reset();
|
|
mocks.executeShellCommand.mockReset();
|
|
});
|
|
|
|
it('applies current UI state to the shared store', async () => {
|
|
const uiState = createUiState('stopped but enabled', 0);
|
|
|
|
mocks.executeShellCommand.mockResolvedValue({
|
|
stdout: JSON.stringify(uiState),
|
|
stderr: '',
|
|
code: 0,
|
|
});
|
|
|
|
await expect(refreshRuntimeUiState({ force: true })).resolves.toEqual(
|
|
uiState,
|
|
);
|
|
|
|
expect(mocks.executeShellCommand).toHaveBeenCalledWith({
|
|
command: '/usr/bin/tachyon',
|
|
args: ['get_ui_state'],
|
|
timeout: 3000,
|
|
});
|
|
expect(store.get().servicesInfoWidget.data).toMatchObject({
|
|
tachyonRunning: 0,
|
|
tachyonEnabled: 1,
|
|
tachyonStatus: 'stopped but enabled',
|
|
});
|
|
});
|
|
|
|
it('coalesces concurrent refreshes into one RPC call', async () => {
|
|
const uiState = createUiState();
|
|
let resolveRpc: (value: {
|
|
stdout: string;
|
|
stderr: string;
|
|
code: number;
|
|
}) => void = () => undefined;
|
|
|
|
mocks.executeShellCommand.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveRpc = resolve;
|
|
}),
|
|
);
|
|
|
|
const firstRefresh = refreshRuntimeUiState({ force: true });
|
|
const secondRefresh = refreshRuntimeUiState({ force: true });
|
|
|
|
expect(mocks.executeShellCommand).toHaveBeenCalledTimes(1);
|
|
|
|
resolveRpc({
|
|
stdout: JSON.stringify(uiState),
|
|
stderr: '',
|
|
code: 0,
|
|
});
|
|
|
|
await expect(Promise.all([firstRefresh, secondRefresh])).resolves.toEqual([
|
|
uiState,
|
|
uiState,
|
|
]);
|
|
});
|
|
|
|
it('notifies subscribers after applying fresh state', async () => {
|
|
const uiState = createUiState('stopped but enabled', 0);
|
|
const listener = vi.fn();
|
|
const unsubscribe = subscribeRuntimeUiState(listener);
|
|
listener.mockClear();
|
|
|
|
mocks.executeShellCommand.mockResolvedValue({
|
|
stdout: JSON.stringify(uiState),
|
|
stderr: '',
|
|
code: 0,
|
|
});
|
|
|
|
await refreshRuntimeUiState({ force: true });
|
|
|
|
expect(listener).toHaveBeenCalledWith(uiState);
|
|
|
|
unsubscribe();
|
|
listener.mockClear();
|
|
|
|
await refreshRuntimeUiState({ force: true });
|
|
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('replays cached state to a new subscriber without another RPC', async () => {
|
|
const uiState = createUiState('starting', 0);
|
|
|
|
mocks.executeShellCommand.mockResolvedValue({
|
|
stdout: JSON.stringify(uiState),
|
|
stderr: '',
|
|
code: 0,
|
|
});
|
|
|
|
await refreshRuntimeUiState({ force: true });
|
|
|
|
const listener = vi.fn();
|
|
const unsubscribe = subscribeRuntimeUiState(listener);
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(uiState);
|
|
expect(mocks.executeShellCommand).toHaveBeenCalledTimes(1);
|
|
|
|
unsubscribe();
|
|
});
|
|
|
|
it('exposes the cached state without another RPC', async () => {
|
|
const uiState = createUiState('running & enabled', 1);
|
|
|
|
mocks.executeShellCommand.mockResolvedValue({
|
|
stdout: JSON.stringify(uiState),
|
|
stderr: '',
|
|
code: 0,
|
|
});
|
|
|
|
await refreshRuntimeUiState({ force: true });
|
|
mocks.executeShellCommand.mockClear();
|
|
|
|
expect(getCachedRuntimeUiState()).toEqual(uiState);
|
|
expect(mocks.executeShellCommand).not.toHaveBeenCalled();
|
|
});
|
|
});
|