Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createRequire} from 'node:module';
|
||||
import {afterEach, describe, expect, test} from 'vitest';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const macAppAudio = require('./index.js');
|
||||
|
||||
function makeFakeBinding() {
|
||||
const calls = [];
|
||||
const natives = [];
|
||||
class FakeNative {
|
||||
constructor() {
|
||||
this.frameCallback = undefined;
|
||||
this.lifecycleCallback = undefined;
|
||||
this.stopCount = 0;
|
||||
natives.push(this);
|
||||
}
|
||||
|
||||
setFrameCallback(callback) {
|
||||
this.frameCallback = callback;
|
||||
}
|
||||
|
||||
setLifecycleCallback(callback) {
|
||||
this.lifecycleCallback = callback;
|
||||
}
|
||||
|
||||
async start(pid, excludeSelf, includeProcessTree, backend, captureScope) {
|
||||
calls.push({pid, excludeSelf, includeProcessTree, backend, captureScope});
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopCount += 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
binding: {
|
||||
ProcessLoopback: FakeNative,
|
||||
listAudibleApplications: async () => [],
|
||||
getBackendAvailability: async () => ({}),
|
||||
pidFromWindowId: () => 0,
|
||||
},
|
||||
calls,
|
||||
natives,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
macAppAudio.__setBindingForTests(null);
|
||||
});
|
||||
|
||||
describe('mac-app-audio loader wrapper', () => {
|
||||
test('forces excludeSelf=true and preserves includeProcessTree=false', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234, {
|
||||
excludeSelf: false,
|
||||
includeProcessTree: false,
|
||||
});
|
||||
loopback.on('error', () => {});
|
||||
await loopback.start();
|
||||
expect(calls).toEqual([
|
||||
{pid: 1234, excludeSelf: true, includeProcessTree: false, backend: 'auto', captureScope: 'process'},
|
||||
]);
|
||||
});
|
||||
test('forwards explicit backend preference to native binding', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234, {
|
||||
backend: 'coreaudio',
|
||||
});
|
||||
loopback.on('error', () => {});
|
||||
await loopback.start();
|
||||
expect(calls).toEqual([
|
||||
{pid: 1234, excludeSelf: true, includeProcessTree: true, backend: 'coreaudio', captureScope: 'process'},
|
||||
]);
|
||||
});
|
||||
test('forwards explicit capture scope to native binding', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234, {
|
||||
captureScope: 'system',
|
||||
backend: 'coreaudio',
|
||||
});
|
||||
loopback.on('error', () => {});
|
||||
await loopback.start();
|
||||
expect(calls).toEqual([
|
||||
{pid: 1234, excludeSelf: true, includeProcessTree: true, backend: 'coreaudio', captureScope: 'system'},
|
||||
]);
|
||||
});
|
||||
test('forwards ScreenCaptureKit system capture requests to native binding', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234, {
|
||||
captureScope: 'system',
|
||||
backend: 'sck',
|
||||
});
|
||||
loopback.on('error', () => {});
|
||||
await loopback.start();
|
||||
expect(calls).toEqual([
|
||||
{pid: 1234, excludeSelf: true, includeProcessTree: true, backend: 'sck', captureScope: 'system'},
|
||||
]);
|
||||
});
|
||||
test('copies native frame samples before emitting', () => {
|
||||
const {binding, natives} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234);
|
||||
const frames = [];
|
||||
loopback.on('frame', (frame) => frames.push(frame));
|
||||
const nativeSamples = new Float32Array([0.1, 0.2, 0.3, 0.4]);
|
||||
natives[0].frameCallback(nativeSamples);
|
||||
nativeSamples.fill(9);
|
||||
expect(frames).toHaveLength(1);
|
||||
expect(Array.from(frames[0].samples)).toEqual(Array.from(new Float32Array([0.1, 0.2, 0.3, 0.4])));
|
||||
expect(frames[0].channels).toBe(2);
|
||||
expect(frames[0].sampleRate).toBe(48000);
|
||||
});
|
||||
test('emits closed once when native lifecycle closes and stop is called later', async () => {
|
||||
const {binding, natives} = makeFakeBinding();
|
||||
macAppAudio.__setBindingForTests(binding);
|
||||
const loopback = new macAppAudio.ProcessLoopback(1234);
|
||||
let closed = 0;
|
||||
loopback.on('closed', () => {
|
||||
closed += 1;
|
||||
});
|
||||
natives[0].lifecycleCallback('closed', '');
|
||||
await Promise.resolve();
|
||||
await loopback.stop();
|
||||
expect(closed).toBe(1);
|
||||
expect(natives[0].stopCount).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user