Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7e85f7b
Refactor shared and SSH Effect services
juliusmarminge Jun 20, 2026
57b8f56
Preserve SSH and network error messages
juliusmarminge Jun 20, 2026
af2366a
Keep structured SSH errors stable
juliusmarminge Jun 20, 2026
1916732
Fix SSH auth retries through structured causes
juliusmarminge Jun 20, 2026
c631c70
Limit SSH auth retries to process causes
juliusmarminge Jun 20, 2026
71558c1
Use exhaustive relay install error handling
juliusmarminge Jun 20, 2026
8f0c480
Inline SSH tunnel cancellation error
juliusmarminge Jun 20, 2026
f8f9364
Keep relay install error boundary explicit
juliusmarminge Jun 20, 2026
2fc3ddd
Use exhaustive relay client error handling
juliusmarminge Jun 20, 2026
1b75e73
Inline relay install error mappings
juliusmarminge Jun 20, 2026
101ea19
Preserve SSH readiness diagnostics
juliusmarminge Jun 20, 2026
4b8a554
fix: bound SSH failure diagnostics
juliusmarminge Jun 20, 2026
1d53e19
fix(ssh): sanitize URL diagnostics
juliusmarminge Jun 20, 2026
4e1cdad
[codex] Redact SSH tunnel diagnostics (#3412)
juliusmarminge Jun 20, 2026
8abe5f1
[codex] Structure loopback port errors (#3358)
juliusmarminge Jun 20, 2026
0d94e22
[codex] Preserve SSH exit diagnostics
juliusmarminge Jun 20, 2026
c0f2716
[codex] Keep desktop path probes observable
juliusmarminge Jun 20, 2026
40a3253
[codex] Correlate tunnel failures without aliases
juliusmarminge Jun 20, 2026
14159ea
[codex] Trim generator refactor tests
juliusmarminge Jun 20, 2026
ceb4fdc
Inline environment authorization errors
juliusmarminge Jun 20, 2026
2b84cc0
Normalize SSH timeout targets
juliusmarminge Jun 20, 2026
1955084
[codex] Structure command resolution failures (#3441)
juliusmarminge Jun 20, 2026
477a96f
Clarify SSH IPC error presentation
juliusmarminge Jun 20, 2026
2019140
Schema encode SSH request errors
juliusmarminge Jun 21, 2026
44dd90f
Structure relay install transport errors
juliusmarminge Jun 21, 2026
9ed4bf4
fix(desktop): preserve structural SSH IPC errors
juliusmarminge Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";

Expand Down Expand Up @@ -36,6 +37,14 @@ interface ElectronAppCalls {
readonly setName: string[];
}

function flattenedLogText(value: unknown): string {
if (typeof value === "string") return value;
if (typeof value !== "object" || value === null) return String(value);
return Object.entries(value)
.flatMap(([key, nested]) => [key, flattenedLogText(nested)])
.join("\n");
}

const makeElectronAppLayer = (calls: ElectronAppCalls) =>
Layer.succeed(ElectronApp.ElectronApp, {
metadata: Effect.die("unexpected metadata read"),
Expand Down Expand Up @@ -123,7 +132,7 @@ const withIdentity = <A, E, R>(
Layer.provideMerge(
FileSystem.layerNoop({
exists: (path) =>
input.legacyPathProbeError
path.includes("T3 Code (Alpha)") && input.legacyPathProbeError !== undefined
? Effect.fail(input.legacyPathProbeError)
: Effect.succeed(
input.legacyPathExists === true && path.includes("T3 Code (Alpha)"),
Expand Down Expand Up @@ -153,31 +162,32 @@ describe("DesktopAppIdentity", () => {
),
);

it.effect("preserves failures while inspecting the legacy userData path", () => {
const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)";
const cause = PlatformError.systemError({
it.effect("falls back and reports a failed legacy userData path probe", () => {
const probeError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
description: "permission denied",
pathOrDescriptor: legacyPath,
pathOrDescriptor: "/Users/alice/Library/Application Support/T3 Code (Alpha)",
});
const capturedLogs: Array<ReadonlyArray<unknown>> = [];
const logger = Logger.make(({ message }) => {
capturedLogs.push(Array.isArray(message) ? message : [message]);
});

return withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
const error = yield* identity.resolveUserDataPath.pipe(Effect.flip);

assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError);
assert.equal(error.legacyPath, legacyPath);
assert.strictEqual(error.cause, cause);
assert.equal(
error.message,
`Failed to inspect legacy desktop user-data path at "${legacyPath}".`,
);
const userDataPath = yield* identity.resolveUserDataPath;

assert.equal(userDataPath, "/Users/alice/Library/Application Support/t3code");
const logText = flattenedLogText(capturedLogs);
assert.include(logText, "desktop.appIdentity.legacyUserDataProbe.failed");
assert.include(logText, "/Users/alice/Library/Application Support/T3 Code (Alpha)");
assert.include(logText, "/Users/alice/Library/Application Support/t3code");
assert.include(logText, "PermissionDenied");
}),
{ legacyPathProbeError: cause },
);
{ legacyPathProbeError: probeError },
).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false })));
});

it.effect("configures app identity from the environment commit override", () => {
Expand Down
35 changes: 13 additions & 22 deletions apps/desktop/src/app/DesktopAppIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,10 @@ const AppPackageMetadata = Schema.Struct({
});
const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));

export class DesktopUserDataPathResolutionError extends Schema.TaggedErrorClass<DesktopUserDataPathResolutionError>()(
"DesktopUserDataPathResolutionError",
{
legacyPath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to inspect legacy desktop user-data path at "${this.legacyPath}".`;
}
}

export class DesktopAppIdentity extends Context.Service<
DesktopAppIdentity,
{
readonly resolveUserDataPath: Effect.Effect<string, DesktopUserDataPathResolutionError>;
readonly resolveUserDataPath: Effect.Effect<string>;
readonly configure: Effect.Effect<void>;
}
>()("@t3tools/desktop/app/DesktopAppIdentity") {}
Expand Down Expand Up @@ -95,18 +83,21 @@ export const make = Effect.gen(function* () {
environment.appDataDirectory,
environment.legacyUserDataDirName,
);
const fallbackPath = environment.path.join(
environment.appDataDirectory,
environment.userDataDirName,
);
const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
Effect.mapError(
(cause) =>
new DesktopUserDataPathResolutionError({
legacyPath,
cause,
}),
Effect.tapError((cause) =>
Effect.logWarning("desktop.appIdentity.legacyUserDataProbe.failed", {
legacyPath,
fallbackPath,
cause,
}),
),
Effect.orElseSucceed(() => false),
);
return legacyPathExists
? legacyPath
: environment.path.join(environment.appDataDirectory, environment.userDataDirName);
return legacyPathExists ? legacyPath : fallbackPath;
}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath"));

const configure = Effect.gen(function* () {
Expand Down
89 changes: 53 additions & 36 deletions apps/desktop/src/app/DesktopAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,72 @@ 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 Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";

import * as DesktopAssets from "./DesktopAssets.ts";
import * as DesktopConfig from "./DesktopConfig.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

const environmentLayer = DesktopEnvironment.layer({
const environmentInput = {
dirname: "/repo/apps/desktop/dist-electron",
homeDirectory: "/Users/alice",
platform: "darwin",
processArch: "arm64",
platform: "linux",
processArch: "x64",
appVersion: "1.2.3",
appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar",
appPath: "/repo",
isPackaged: true,
resourcesPath: "/Applications/T3 Code.app/Contents/Resources",
resourcesPath: "/resources",
runningUnderArm64Translation: false,
}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))));
} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;

function flattenedLogText(value: unknown): string {
if (typeof value === "string") return value;
if (typeof value !== "object" || value === null) return String(value);
return Object.entries(value)
.flatMap(([key, nested]) => [key, flattenedLogText(nested)])
.join("\n");
}

describe("DesktopAssets", () => {
it.effect("preserves the failed asset candidate and filesystem cause", () =>
Effect.gen(function* () {
const fileName = "custom.bin";
const candidatePath = "/repo/apps/desktop/resources/custom.bin";
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
pathOrDescriptor: candidatePath,
description: "private filesystem diagnostic",
});
const fileSystemLayer = FileSystem.layerNoop({
exists: (path) => (path === candidatePath ? Effect.fail(cause) : Effect.succeed(false)),
});
const assetsLayer = DesktopAssets.layer.pipe(
Layer.provide(Layer.merge(fileSystemLayer, environmentLayer)),
);
const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(assetsLayer));
it.effect("continues resource lookup and reports failed existence probes", () => {
const firstCandidate = "/repo/apps/desktop/resources/icon.ico";
const fallbackCandidate = "/repo/apps/desktop/prod-resources/icon.ico";
const probeError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
pathOrDescriptor: firstCandidate,
});
const capturedLogs: Array<ReadonlyArray<unknown>> = [];
const logger = Logger.make(({ message }) => {
capturedLogs.push(Array.isArray(message) ? message : [message]);
});
const fileSystemLayer = FileSystem.layerNoop({
exists: (path) => {
if (path === firstCandidate) return Effect.fail(probeError);
return Effect.succeed(path === fallbackCandidate);
},
});
const environmentLayer = DesktopEnvironment.layer(environmentInput).pipe(
Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
);
const assetsLayer = DesktopAssets.layer.pipe(
Layer.provideMerge(fileSystemLayer),
Layer.provideMerge(environmentLayer),
Layer.provide(Logger.layer([logger], { mergeWithExisting: false })),
);

const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);
return Effect.gen(function* () {
const assets = yield* DesktopAssets.DesktopAssets;
const iconPaths = yield* assets.iconPaths;

assert.instanceOf(error, DesktopAssets.DesktopAssetProbeError);
assert.equal(error.fileName, fileName);
assert.equal(error.candidatePath, candidatePath);
assert.strictEqual(error.cause, cause);
assert.equal(
error.message,
`Failed to probe desktop asset "${fileName}" at ${candidatePath}.`,
);
assert.notInclude(error.message, "private filesystem diagnostic");
}),
);
assert.deepEqual(iconPaths.ico, Option.some(fallbackCandidate));
const logText = flattenedLogText(capturedLogs);
assert.include(logText, "desktop.assets.resourceProbe.failed");
assert.include(logText, firstCandidate);
assert.include(logText, "PermissionDenied");
}).pipe(Effect.provide(assetsLayer));
});
});
53 changes: 20 additions & 33 deletions apps/desktop/src/app/DesktopAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as DesktopEnvironment from "./DesktopEnvironment.ts";

Expand All @@ -13,47 +12,35 @@ export interface DesktopIconPaths {
readonly png: Option.Option<string>;
}

export class DesktopAssetProbeError extends Schema.TaggedErrorClass<DesktopAssetProbeError>()(
"DesktopAssetProbeError",
{
fileName: Schema.String,
candidatePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to probe desktop asset "${this.fileName}" at ${this.candidatePath}.`;
}
}

export class DesktopAssets extends Context.Service<
DesktopAssets,
{
readonly iconPaths: Effect.Effect<DesktopIconPaths>;
readonly resolveResourcePath: (
fileName: string,
) => Effect.Effect<Option.Option<string>, DesktopAssetProbeError>;
readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;
}
>()("@t3tools/desktop/app/DesktopAssets") {}

const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(function* (
fileName: string,
): Effect.fn.Return<
Option.Option<string>,
DesktopAssetProbeError,
never,
FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
> {
const fileSystem = yield* FileSystem.FileSystem;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const candidates = environment.resolveResourcePathCandidates(fileName);
for (const candidate of candidates) {
const exists = yield* fileSystem
.exists(candidate)
.pipe(
Effect.mapError(
(cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
),
);
const exists = yield* fileSystem.exists(candidate).pipe(
Effect.tapError((cause) =>
Effect.logWarning("desktop.assets.resourceProbe.failed", {
fileName,
candidatePath: candidate,
cause,
}),
),
Effect.orElseSucceed(() => false),
);
if (exists) {
return Option.some(candidate);
}
Expand All @@ -65,22 +52,22 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
ext: keyof DesktopIconPaths,
): Effect.fn.Return<
Option.Option<string>,
DesktopAssetProbeError,
never,
FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
> {
const fileSystem = yield* FileSystem.FileSystem;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
const developmentDockIconPath = environment.developmentDockIconPath;
const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe(
Effect.mapError(
(cause) =>
new DesktopAssetProbeError({
fileName: "icon.png",
candidatePath: developmentDockIconPath,
cause,
}),
Effect.tapError((cause) =>
Effect.logWarning("desktop.assets.resourceProbe.failed", {
fileName: "development-dock-icon",
candidatePath: developmentDockIconPath,
cause,
}),
),
Effect.orElseSucceed(() => false),
);
if (developmentDockIconExists) {
return Option.some(developmentDockIconPath);
Expand Down
Loading
Loading