import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts";import * as DesktopServerExposure from "./DesktopServerExposure.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; const encoder = new TextEncoder(); const emptyNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {};const lanNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = { en0: [ { address: "192.168.1.20", family: "IPv4", internal: false, }, ],}; const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = { tailscale0: [ { address: "100.90.1.2", family: "IPv4", internal: false, }, ],}; function mockSpawnerLayer(statusJson = "{}") { return Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.succeed( ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: Stream.make(encoder.encode(statusJson)), stderr: Stream.empty, all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }), ), ), );} function dieOnSpawnLayer() { return Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")), );} function makeEnvironmentLayer(baseDir: string, env: Record<string, string | undefined> = {}) { return DesktopEnvironment.layer({ dirname: "/repo/apps/desktop/src", homeDirectory: baseDir, platform: "darwin", processArch: "x64", appVersion: "1.2.3", appPath: "/repo", isPackaged: true, resourcesPath: "/missing/resources", runningUnderArm64Translation: false, }).pipe( Layer.provide( Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir, ...env })), ), );} function makeLayer(input: { readonly baseDir: string; readonly networkInterfaces?: DesktopNetworkInterfaces.NetworkInterfaces; readonly env?: Record<string, string | undefined>; readonly spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>; readonly desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>;}) { const env = { T3CODE_HOME: input.baseDir, ...input.env }; const environmentLayer = makeEnvironmentLayer(input.baseDir, env); const networkLayer = Layer.succeed(DesktopNetworkInterfaces.DesktopNetworkInterfaces, { read: Effect.succeed(input.networkInterfaces ?? emptyNetworkInterfaces), }); return DesktopServerExposure.layer.pipe( Layer.provideMerge(input.desktopSettingsLayer ?? DesktopAppSettings.layer), Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(NodeHttpClient.layerUndici), Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()), Layer.provideMerge(networkLayer), Layer.provideMerge(DesktopConfig.layerTest(env)), Layer.provideMerge(environmentLayer), );} const withHarness = <A, E, R>( networkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces, effect: Effect.Effect< A, E, | R | DesktopEnvironment.DesktopEnvironment | FileSystem.FileSystem | DesktopServerExposure.DesktopServerExposure | DesktopAppSettings.DesktopAppSettings >, env: Record<string, string | undefined> = {}, spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>, desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>,) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-desktop-server-exposure-test-", }); return yield* effect.pipe( Effect.provide( makeLayer({ baseDir, networkInterfaces, env, ...(spawnerLayer ? { spawnerLayer } : {}), ...(desktopSettingsLayer ? { desktopSettingsLayer } : {}), }), ), ); }).pipe(Effect.provide(NodeServices.layer), Effect.scoped); describe("DesktopServerExposure", () => { it.effect("falls back to local-only without losing the requested network preference", () => withHarness( emptyNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const settings = yield* DesktopAppSettings.DesktopAppSettings; yield* settings.setServerExposureMode("network-accessible"); const state = yield* serverExposure.configureFromSettings({ port: 4173 }); assert.equal(state.mode, "local-only"); assert.equal(state.endpointUrl, null); assert.equal((yield* settings.get).serverExposureMode, "network-accessible"); const backendConfig = yield* serverExposure.backendConfig; assert.equal(backendConfig.bindHost, "127.0.0.1"); assert.equal(backendConfig.httpBaseUrl.href, "http://127.0.0.1:4173/"); }), ), ); it.effect("returns a typed error when network access is explicitly unavailable", () => withHarness( emptyNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); const error = yield* serverExposure.setMode("network-accessible").pipe(Effect.flip); assert.ok(error._tag === "DesktopServerExposureNoNetworkAddressError"); assert.equal(error.port, 4173); }), ), ); it.effect("persists network-accessible mode and updates backend binding state", () => withHarness( lanNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const settings = yield* DesktopAppSettings.DesktopAppSettings; yield* settings.load; yield* serverExposure.configureFromSettings({ port: 4173 }); const change = yield* serverExposure.setMode("network-accessible"); assert.equal(change.requiresRelaunch, true); assert.deepEqual(change.state, { mode: "network-accessible", endpointUrl: "http://192.168.1.20:4173", advertisedHost: "192.168.1.20", tailscaleServeEnabled: false, tailscaleServePort: 443, }); const backendConfig = yield* serverExposure.backendConfig; assert.equal(backendConfig.bindHost, "0.0.0.0"); assert.equal(backendConfig.httpBaseUrl.href, "http://127.0.0.1:4173/"); const persisted = yield* settings.get; assert.equal(persisted.serverExposureMode, "network-accessible"); }), ), ); it.effect("persists tailscale serve preferences atomically and reports no-op updates", () => withHarness( emptyNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const settings = yield* DesktopAppSettings.DesktopAppSettings; yield* settings.load; yield* serverExposure.configureFromSettings({ port: 4173 }); const changed = yield* serverExposure.setTailscaleServeEnabled({ enabled: true, port: 8443, }); assert.equal(changed.requiresRelaunch, true); assert.equal(changed.state.tailscaleServeEnabled, true); assert.equal(changed.state.tailscaleServePort, 8443); const unchanged = yield* serverExposure.setTailscaleServeEnabled({ enabled: true, port: 8443, }); assert.equal(unchanged.requiresRelaunch, false); const persisted = yield* settings.get; assert.equal(persisted.tailscaleServeEnabled, true); assert.equal(persisted.tailscaleServePort, 8443); }), ), ); it.effect("preserves persistence request context and the settings failure chain", () => { const diskFailure = new Error("disk exploded"); const settingsFailure = new DesktopAppSettings.DesktopSettingsWriteError({ operation: "replace-settings-file", path: "/tmp/desktop-settings.json", cause: diskFailure, }); const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), setMainWindowBounds: () => Effect.die("unexpected main window bounds update"), setServerExposureMode: () => Effect.fail(settingsFailure), setTailscaleServe: () => Effect.fail(settingsFailure), setUpdateChannel: () => Effect.die("unexpected update channel change"), setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"), setWslDistro: () => Effect.die("unexpected WSL distro change"), setWslOnly: () => Effect.die("unexpected WSL-only toggle"), applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"), applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"), } satisfies DesktopAppSettings.DesktopAppSettings["Service"]); return withHarness( lanNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); const modeError = yield* serverExposure.setMode("network-accessible").pipe(Effect.flip); assert.instanceOf( modeError, DesktopServerExposure.DesktopServerExposureModePersistenceError, ); assert.equal(modeError.mode, "network-accessible"); assert.strictEqual(modeError.cause, settingsFailure); assert.strictEqual(modeError.cause.cause, diskFailure); assert.equal( modeError.message, "Failed to persist desktop server exposure mode network-accessible.", ); assert.notInclude(modeError.message, diskFailure.message); const tailscaleError = yield* serverExposure .setTailscaleServeEnabled({ enabled: true, port: 8443 }) .pipe(Effect.flip); assert.instanceOf( tailscaleError, DesktopServerExposure.DesktopTailscaleServePersistenceError, ); assert.equal(tailscaleError.enabled, true); assert.equal(tailscaleError.port, 8443); assert.strictEqual(tailscaleError.cause, settingsFailure); assert.strictEqual(tailscaleError.cause.cause, diskFailure); assert.equal( tailscaleError.message, "Failed to persist desktop Tailscale Serve settings (enabled: true, port: 8443).", ); assert.notInclude(tailscaleError.message, diskFailure.message); }), {}, undefined, settingsLayer, ); }); it.effect("keeps LAN and Tailscale endpoints distinct when Tailscale is enumerated first", () => withHarness( { ...tailnetNetworkInterfaces, ...lanNetworkInterfaces }, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); yield* serverExposure.setMode("network-accessible"); const endpoints = yield* serverExposure.getAdvertisedEndpoints; assert.deepEqual( endpoints.map((endpoint) => endpoint.httpBaseUrl), ["http://127.0.0.1:4173/", "http://192.168.1.20:4173/", "http://100.90.1.2:4173/"], ); }), ), ); it.effect("keeps Tailscale-only hosts network-accessible", () => withHarness( tailnetNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const settings = yield* DesktopAppSettings.DesktopAppSettings; yield* settings.setServerExposureMode("network-accessible"); const state = yield* serverExposure.configureFromSettings({ port: 4173 }); assert.equal(state.mode, "network-accessible"); assert.equal(state.advertisedHost, null); assert.equal(state.endpointUrl, null); assert.equal((yield* serverExposure.backendConfig).bindHost, "0.0.0.0"); const endpoints = yield* serverExposure.getAdvertisedEndpoints; assert.deepEqual( endpoints.map((endpoint) => [endpoint.reachability, endpoint.httpBaseUrl]), [ ["loopback", "http://127.0.0.1:4173/"], ["private-network", "http://100.90.1.2:4173/"], ], ); }), ), ); it.effect("does not spawn the tailscale CLI while server exposure is local-only", () => withHarness( lanNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); // mode stays at default "local-only", tailscaleServeEnabled stays false. const endpoints = yield* serverExposure.getAdvertisedEndpoints; // Only the loopback endpoint; no tailscale spawn means the dieOnSpawnLayer // would have crashed the test if the gate was missing. assert.deepEqual( endpoints.map((endpoint) => endpoint.httpBaseUrl), ["http://127.0.0.1:4173/"], ); }), {}, dieOnSpawnLayer(), ), ); it.effect("preserves explicit Tailscale exposure overrides", () => withHarness( lanNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); const change = yield* serverExposure.setMode("network-accessible"); assert.equal(change.state.advertisedHost, "100.90.1.2"); assert.equal(change.state.endpointUrl, "http://100.90.1.2:4173"); const endpoints = yield* serverExposure.getAdvertisedEndpoints; assert.deepEqual( endpoints.map((endpoint) => endpoint.httpBaseUrl), ["http://127.0.0.1:4173/", "http://100.90.1.2:4173/", "https://public.example.test/"], ); }), { T3CODE_DESKTOP_LAN_HOST: "100.90.1.2", T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", }, ), ); it.effect("advertises loopback, LAN, and configured manual endpoints from runtime state", () => withHarness( lanNetworkInterfaces, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 3773 }); yield* serverExposure.setMode("network-accessible"); const endpoints = yield* serverExposure.getAdvertisedEndpoints; assert.deepEqual(endpoints, [ { id: "desktop-loopback:3773", label: "This machine", provider: { id: "desktop-core", label: "Desktop", kind: "core", isAddon: false, }, httpBaseUrl: "http://127.0.0.1:3773/", wsBaseUrl: "ws://127.0.0.1:3773/", reachability: "loopback", compatibility: { hostedHttpsApp: "mixed-content-blocked", desktopApp: "compatible", }, source: "desktop-core", status: "available", description: "Loopback endpoint for this desktop app.", }, { id: "desktop-lan:http://192.168.1.20:3773", label: "Local network", provider: { id: "desktop-core", label: "Desktop", kind: "core", isAddon: false, }, httpBaseUrl: "http://192.168.1.20:3773/", wsBaseUrl: "ws://192.168.1.20:3773/", reachability: "lan", compatibility: { hostedHttpsApp: "mixed-content-blocked", desktopApp: "compatible", }, source: "desktop-core", status: "available", isDefault: true, description: "Reachable from devices on the same network.", }, { id: "manual:https://desktop.example.ts.net", label: "Custom HTTPS", provider: { id: "manual", label: "Manual", kind: "manual", isAddon: false, }, httpBaseUrl: "https://desktop.example.ts.net/", wsBaseUrl: "wss://desktop.example.ts.net/", reachability: "public", compatibility: { hostedHttpsApp: "compatible", desktopApp: "compatible", }, source: "user", status: "unknown", description: "User-configured HTTPS endpoint for this desktop backend.", }, { id: "manual:http://desktop.example.test:3773", label: "Custom endpoint", provider: { id: "manual", label: "Manual", kind: "manual", isAddon: false, }, httpBaseUrl: "http://desktop.example.test:3773/", wsBaseUrl: "ws://desktop.example.test:3773/", reachability: "public", compatibility: { hostedHttpsApp: "mixed-content-blocked", desktopApp: "compatible", }, source: "user", status: "unknown", description: "User-configured endpoint for this desktop backend.", }, ]); }), { T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://desktop.example.ts.net,http://desktop.example.test:3773,not-a-url", }, ), );});
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopConfig from "../app/DesktopConfig.ts";
import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts";
import * as DesktopServerExposure from "./DesktopServerExposure.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
const encoder = new TextEncoder();
const emptyNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {};
const lanNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
en0: [
{
address: "192.168.1.20",
family: "IPv4",
internal: false,
},
],
};
const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
tailscale0: [
{
address: "100.90.1.2",
family: "IPv4",
internal: false,
},
],
};
function mockSpawnerLayer(statusJson = "{}") {
return Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() =>
Effect.succeed(
ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
stdin: Sink.drain,
stdout: Stream.make(encoder.encode(statusJson)),
stderr: Stream.empty,
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
}),
),
),
);
}
function dieOnSpawnLayer() {
return Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")),
);
}
function makeEnvironmentLayer(baseDir: string, env: Record<string, string | undefined> = {}) {
return DesktopEnvironment.layer({
dirname: "/repo/apps/desktop/src",
homeDirectory: baseDir,
platform: "darwin",
processArch: "x64",
appVersion: "1.2.3",
appPath: "/repo",
isPackaged: true,
resourcesPath: "/missing/resources",
runningUnderArm64Translation: false,
}).pipe(
Layer.provide(
Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir, ...env })),
),
);
}
function makeLayer(input: {
readonly baseDir: string;
readonly networkInterfaces?: DesktopNetworkInterfaces.NetworkInterfaces;
readonly env?: Record<string, string | undefined>;
readonly spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>;
readonly desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>;
}) {
const env = { T3CODE_HOME: input.baseDir, ...input.env };
const environmentLayer = makeEnvironmentLayer(input.baseDir, env);
const networkLayer = Layer.succeed(DesktopNetworkInterfaces.DesktopNetworkInterfaces, {
read: Effect.succeed(input.networkInterfaces ?? emptyNetworkInterfaces),
});
return DesktopServerExposure.layer.pipe(
Layer.provideMerge(input.desktopSettingsLayer ?? DesktopAppSettings.layer),
Layer.provideMerge(NodeFileSystem.layer),
Layer.provideMerge(NodeHttpClient.layerUndici),
Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()),
Layer.provideMerge(networkLayer),
Layer.provideMerge(DesktopConfig.layerTest(env)),
Layer.provideMerge(environmentLayer),
);
}
const withHarness = <A, E, R>(
networkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces,
effect: Effect.Effect<
A,
E,
| R
| DesktopEnvironment.DesktopEnvironment
| FileSystem.FileSystem
| DesktopServerExposure.DesktopServerExposure
| DesktopAppSettings.DesktopAppSettings
>,
env: Record<string, string | undefined> = {},
spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>,
desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>,
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-server-exposure-test-",
});
return yield* effect.pipe(
Effect.provide(
makeLayer({
baseDir,
networkInterfaces,
env,
...(spawnerLayer ? { spawnerLayer } : {}),
...(desktopSettingsLayer ? { desktopSettingsLayer } : {}),
}),
),
);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
describe("DesktopServerExposure", () => {
it.effect("falls back to local-only without losing the requested network preference", () =>
withHarness(
emptyNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
const state = yield* serverExposure.configureFromSettings({ port: 4173 });
assert.equal(state.mode, "local-only");
assert.equal(state.endpointUrl, null);
assert.equal((yield* settings.get).serverExposureMode, "network-accessible");
const backendConfig = yield* serverExposure.backendConfig;
assert.equal(backendConfig.bindHost, "127.0.0.1");
assert.equal(backendConfig.httpBaseUrl.href, "http://127.0.0.1:4173/");
}),
),
);
it.effect("returns a typed error when network access is explicitly unavailable", () =>
withHarness(
emptyNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
const error = yield* serverExposure.setMode("network-accessible").pipe(Effect.flip);
assert.ok(error._tag === "DesktopServerExposureNoNetworkAddressError");
assert.equal(error.port, 4173);
}),
),
);
it.effect("persists network-accessible mode and updates backend binding state", () =>
withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.load;
yield* serverExposure.configureFromSettings({ port: 4173 });
const change = yield* serverExposure.setMode("network-accessible");
assert.equal(change.requiresRelaunch, true);
assert.deepEqual(change.state, {
mode: "network-accessible",
endpointUrl: "http://192.168.1.20:4173",
advertisedHost: "192.168.1.20",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
const backendConfig = yield* serverExposure.backendConfig;
assert.equal(backendConfig.bindHost, "0.0.0.0");
assert.equal(backendConfig.httpBaseUrl.href, "http://127.0.0.1:4173/");
const persisted = yield* settings.get;
assert.equal(persisted.serverExposureMode, "network-accessible");
}),
),
);
it.effect("persists tailscale serve preferences atomically and reports no-op updates", () =>
withHarness(
emptyNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.load;
yield* serverExposure.configureFromSettings({ port: 4173 });
const changed = yield* serverExposure.setTailscaleServeEnabled({
enabled: true,
port: 8443,
});
assert.equal(changed.requiresRelaunch, true);
assert.equal(changed.state.tailscaleServeEnabled, true);
assert.equal(changed.state.tailscaleServePort, 8443);
const unchanged = yield* serverExposure.setTailscaleServeEnabled({
enabled: true,
port: 8443,
});
assert.equal(unchanged.requiresRelaunch, false);
const persisted = yield* settings.get;
assert.equal(persisted.tailscaleServeEnabled, true);
assert.equal(persisted.tailscaleServePort, 8443);
}),
),
);
it.effect("preserves persistence request context and the settings failure chain", () => {
const diskFailure = new Error("disk exploded");
const settingsFailure = new DesktopAppSettings.DesktopSettingsWriteError({
operation: "replace-settings-file",
path: "/tmp/desktop-settings.json",
cause: diskFailure,
});
const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),
setWslDistro: () => Effect.die("unexpected WSL distro change"),
setWslOnly: () => Effect.die("unexpected WSL-only toggle"),
applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"),
applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),
} satisfies DesktopAppSettings.DesktopAppSettings["Service"]);
return withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
const modeError = yield* serverExposure.setMode("network-accessible").pipe(Effect.flip);
assert.instanceOf(
modeError,
DesktopServerExposure.DesktopServerExposureModePersistenceError,
);
assert.equal(modeError.mode, "network-accessible");
assert.strictEqual(modeError.cause, settingsFailure);
assert.strictEqual(modeError.cause.cause, diskFailure);
assert.equal(
modeError.message,
"Failed to persist desktop server exposure mode network-accessible.",
);
assert.notInclude(modeError.message, diskFailure.message);
const tailscaleError = yield* serverExposure
.setTailscaleServeEnabled({ enabled: true, port: 8443 })
.pipe(Effect.flip);
assert.instanceOf(
tailscaleError,
DesktopServerExposure.DesktopTailscaleServePersistenceError,
);
assert.equal(tailscaleError.enabled, true);
assert.equal(tailscaleError.port, 8443);
assert.strictEqual(tailscaleError.cause, settingsFailure);
assert.strictEqual(tailscaleError.cause.cause, diskFailure);
assert.equal(
tailscaleError.message,
"Failed to persist desktop Tailscale Serve settings (enabled: true, port: 8443).",
);
assert.notInclude(tailscaleError.message, diskFailure.message);
}),
{},
undefined,
settingsLayer,
);
});
it.effect("keeps LAN and Tailscale endpoints distinct when Tailscale is enumerated first", () =>
withHarness(
{ ...tailnetNetworkInterfaces, ...lanNetworkInterfaces },
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
yield* serverExposure.setMode("network-accessible");
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.deepEqual(
endpoints.map((endpoint) => endpoint.httpBaseUrl),
["http://127.0.0.1:4173/", "http://192.168.1.20:4173/", "http://100.90.1.2:4173/"],
);
}),
),
);
it.effect("keeps Tailscale-only hosts network-accessible", () =>
withHarness(
tailnetNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
const state = yield* serverExposure.configureFromSettings({ port: 4173 });
assert.equal(state.mode, "network-accessible");
assert.equal(state.advertisedHost, null);
assert.equal(state.endpointUrl, null);
assert.equal((yield* serverExposure.backendConfig).bindHost, "0.0.0.0");
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.deepEqual(
endpoints.map((endpoint) => [endpoint.reachability, endpoint.httpBaseUrl]),
[
["loopback", "http://127.0.0.1:4173/"],
["private-network", "http://100.90.1.2:4173/"],
],
);
}),
),
);
it.effect("does not spawn the tailscale CLI while server exposure is local-only", () =>
withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
// mode stays at default "local-only", tailscaleServeEnabled stays false.
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
// Only the loopback endpoint; no tailscale spawn means the dieOnSpawnLayer
// would have crashed the test if the gate was missing.
assert.deepEqual(
endpoints.map((endpoint) => endpoint.httpBaseUrl),
["http://127.0.0.1:4173/"],
);
}),
{},
dieOnSpawnLayer(),
),
);
it.effect("preserves explicit Tailscale exposure overrides", () =>
withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
const change = yield* serverExposure.setMode("network-accessible");
assert.equal(change.state.advertisedHost, "100.90.1.2");
assert.equal(change.state.endpointUrl, "http://100.90.1.2:4173");
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.deepEqual(
endpoints.map((endpoint) => endpoint.httpBaseUrl),
["http://127.0.0.1:4173/", "http://100.90.1.2:4173/", "https://public.example.test/"],
);
}),
{
T3CODE_DESKTOP_LAN_HOST: "100.90.1.2",
T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test",
},
),
);
it.effect("advertises loopback, LAN, and configured manual endpoints from runtime state", () =>
withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 3773 });
yield* serverExposure.setMode("network-accessible");
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.deepEqual(endpoints, [
{
id: "desktop-loopback:3773",
label: "This machine",
provider: {
id: "desktop-core",
label: "Desktop",
kind: "core",
isAddon: false,
},
httpBaseUrl: "http://127.0.0.1:3773/",
wsBaseUrl: "ws://127.0.0.1:3773/",
reachability: "loopback",
compatibility: {
hostedHttpsApp: "mixed-content-blocked",
desktopApp: "compatible",
},
source: "desktop-core",
status: "available",
description: "Loopback endpoint for this desktop app.",
},
{
id: "desktop-lan:http://192.168.1.20:3773",
label: "Local network",
provider: {
id: "desktop-core",
label: "Desktop",
kind: "core",
isAddon: false,
},
httpBaseUrl: "http://192.168.1.20:3773/",
wsBaseUrl: "ws://192.168.1.20:3773/",
reachability: "lan",
compatibility: {
hostedHttpsApp: "mixed-content-blocked",
desktopApp: "compatible",
},
source: "desktop-core",
status: "available",
isDefault: true,
description: "Reachable from devices on the same network.",
},
{
id: "manual:https://desktop.example.ts.net",
label: "Custom HTTPS",
provider: {
id: "manual",
label: "Manual",
kind: "manual",
isAddon: false,
},
httpBaseUrl: "https://desktop.example.ts.net/",
wsBaseUrl: "wss://desktop.example.ts.net/",
reachability: "public",
compatibility: {
hostedHttpsApp: "compatible",
desktopApp: "compatible",
},
source: "user",
status: "unknown",
description: "User-configured HTTPS endpoint for this desktop backend.",
},
{
id: "manual:http://desktop.example.test:3773",
label: "Custom endpoint",
provider: {
id: "manual",
label: "Manual",
kind: "manual",
isAddon: false,
},
httpBaseUrl: "http://desktop.example.test:3773/",
wsBaseUrl: "ws://desktop.example.test:3773/",
reachability: "public",
compatibility: {
hostedHttpsApp: "mixed-content-blocked",
desktopApp: "compatible",
},
source: "user",
status: "unknown",
description: "User-configured endpoint for this desktop backend.",
},
]);
}),
{
T3CODE_DESKTOP_HTTPS_ENDPOINTS:
"https://desktop.example.ts.net,http://desktop.example.test:3773,not-a-url",
},
),
);
});