From 406090785befa8cdb51da0cd875183d678450669 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 04:54:15 -0700 Subject: [PATCH] refactor(desktop): structure network interface failures Co-authored-by: codex --- .../backend/DesktopNetworkInterfaces.test.ts | 65 +++++++++++++++++++ .../src/backend/DesktopNetworkInterfaces.ts | 27 ++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts diff --git a/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts b/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts new file mode 100644 index 00000000000..411af7553f9 --- /dev/null +++ b/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts @@ -0,0 +1,65 @@ +import { assert, describe, it } from "@effect/vitest"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { beforeEach, vi } from "vite-plus/test"; + +const { networkInterfacesMock } = vi.hoisted(() => ({ + networkInterfacesMock: vi.fn(), +})); + +vi.mock("node:os", () => ({ + networkInterfaces: networkInterfacesMock, +})); + +import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts"; + +const TestLayer = DesktopNetworkInterfaces.layer.pipe( + Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), +); + +describe("DesktopNetworkInterfaces", () => { + beforeEach(() => { + networkInterfacesMock.mockReset(); + }); + + it.effect("reads network interfaces through the service", () => { + const interfaces = { + en0: [ + { + address: "192.168.1.10", + family: "IPv4", + internal: false, + }, + ], + }; + networkInterfacesMock.mockReturnValueOnce(interfaces); + + return Effect.gen(function* () { + const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces; + assert.strictEqual(yield* service.read, interfaces); + }).pipe(Effect.provide(TestLayer)); + }); + + it.effect("preserves network interface read failures as structured defects", () => { + const cause = new Error("network interface probe failed"); + networkInterfacesMock.mockImplementationOnce(() => { + throw cause; + }); + + return Effect.gen(function* () { + const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces; + const exit = yield* Effect.exit(service.read); + + assert.equal(exit._tag, "Failure"); + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause); + assert.instanceOf(error, DesktopNetworkInterfaces.DesktopNetworkInterfacesReadError); + assert.equal(error.platform, "linux"); + assert.strictEqual(error.cause, cause); + assert.equal(error.message, "Failed to read desktop network interfaces on linux."); + } + }).pipe(Effect.provide(TestLayer)); + }); +}); diff --git a/apps/desktop/src/backend/DesktopNetworkInterfaces.ts b/apps/desktop/src/backend/DesktopNetworkInterfaces.ts index 79b6b824c8a..43f634c4491 100644 --- a/apps/desktop/src/backend/DesktopNetworkInterfaces.ts +++ b/apps/desktop/src/backend/DesktopNetworkInterfaces.ts @@ -1,8 +1,10 @@ import * as NodeOS from "node:os"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; export interface DesktopNetworkInterfaceInfo { readonly address: string; @@ -18,6 +20,18 @@ export type NetworkInterfaces = Readonly< Record >; +export class DesktopNetworkInterfacesReadError extends Schema.TaggedErrorClass()( + "DesktopNetworkInterfacesReadError", + { + platform: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to read desktop network interfaces on ${this.platform}.`; + } +} + export class DesktopNetworkInterfaces extends Context.Service< DesktopNetworkInterfaces, { @@ -25,9 +39,14 @@ export class DesktopNetworkInterfaces extends Context.Service< } >()("@t3tools/desktop/backend/DesktopNetworkInterfaces") {} -export const make = (): DesktopNetworkInterfaces["Service"] => - DesktopNetworkInterfaces.of({ - read: Effect.sync(() => NodeOS.networkInterfaces()), +export const make = Effect.gen(function* () { + const platform = yield* HostProcessPlatform; + return DesktopNetworkInterfaces.of({ + read: Effect.try({ + try: () => NodeOS.networkInterfaces(), + catch: (cause) => new DesktopNetworkInterfacesReadError({ platform, cause }), + }).pipe(Effect.orDie), }); +}); -export const layer = Layer.succeed(DesktopNetworkInterfaces, make()); +export const layer = Layer.effect(DesktopNetworkInterfaces, make);