authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:26:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:26:03-07:00
log48f88d522047198a660cacebbde854a617068720
tree739cea09a4c03fea1a3f5deb67accb6ba65851b6
parent2910d9ff0106d06f8fc1c55a44787385118f595a
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop Clerk bridge failures (#3308)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 114 insertions(+), 15 deletions(-)

apps/desktop/src/app/DesktopClerk.test.ts+67-12
......@@ -1,7 +1,8 @@
11import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
23import * as Effect from "effect/Effect";
34import * as Layer from "effect/Layer";
4import { vi } from "vite-plus/test";
5import { beforeEach, vi } from "vite-plus/test";
56
67const { createClerkBridgeMock, storageAdapter, storageMock } = vi.hoisted(() => ({
78 createClerkBridgeMock: vi.fn(),
......@@ -24,7 +25,23 @@ vi.mock("@clerk/electron/storage", () => ({
2425import * as DesktopClerk from "./DesktopClerk.ts";
2526import * as DesktopEnvironment from "./DesktopEnvironment.ts";
2627
28const makeDesktopClerkLayer = (isDevelopment = true) => {
29 const environment = DesktopEnvironment.DesktopEnvironment.of({
30 stateDir: "/tmp/t3-state",
31 isDevelopment,
32 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
33
34 return DesktopClerk.layer.pipe(
35 Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
36 );
37};
38
2739describe("DesktopClerk", () => {
40 beforeEach(() => {
41 createClerkBridgeMock.mockReset();
42 storageMock.mockReset();
43 });
44
2845 it("derives the Clerk Frontend API hostname used by the desktop CSP", () => {
2946 const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`;
3047
......@@ -40,19 +57,9 @@ describe("DesktopClerk", () => {
4057 const cleanup = vi.fn();
4158 storageMock.mockReturnValue(storageAdapter);
4259 createClerkBridgeMock.mockReturnValue({ cleanup });
43 const environment = DesktopEnvironment.DesktopEnvironment.of({
44 stateDir: "/tmp/t3-state",
45 isDevelopment: true,
46 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
4760
4861 return Effect.gen(function* () {
49 yield* Effect.scoped(
50 Layer.build(
51 DesktopClerk.layer.pipe(
52 Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
53 ),
54 ),
55 );
62 yield* Effect.scoped(Layer.build(makeDesktopClerkLayer()));
5663
5764 assert.deepEqual(createClerkBridgeMock.mock.calls, [
5865 [
......@@ -69,6 +76,54 @@ describe("DesktopClerk", () => {
6976 });
7077 });
7178
79 it.effect("preserves bridge initialization failures", () => {
80 const cause = new Error("bridge initialization failed");
81 storageMock.mockReturnValue(storageAdapter);
82 createClerkBridgeMock.mockImplementationOnce(() => {
83 throw cause;
84 });
85
86 return Effect.gen(function* () {
87 const error = yield* Effect.scoped(Layer.build(makeDesktopClerkLayer())).pipe(Effect.flip);
88
89 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeInitializationError);
90 assert.equal(error.stateDir, "/tmp/t3-state");
91 assert.equal(error.isDevelopment, true);
92 assert.strictEqual(error.cause, cause);
93 assert.equal(
94 error.message,
95 'Failed to initialize the desktop Clerk bridge for state directory "/tmp/t3-state" (development: true).',
96 );
97 });
98 });
99
100 it.effect("preserves bridge cleanup failures", () => {
101 const cause = new Error("bridge cleanup failed");
102 storageMock.mockReturnValue(storageAdapter);
103 createClerkBridgeMock.mockReturnValue({
104 cleanup: () => {
105 throw cause;
106 },
107 });
108
109 return Effect.gen(function* () {
110 const exit = yield* Effect.exit(Effect.scoped(Layer.build(makeDesktopClerkLayer(false))));
111
112 assert.equal(exit._tag, "Failure");
113 if (exit._tag === "Failure") {
114 const error = Cause.squash(exit.cause);
115 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeCleanupError);
116 assert.equal(error.stateDir, "/tmp/t3-state");
117 assert.equal(error.isDevelopment, false);
118 assert.strictEqual(error.cause, cause);
119 assert.equal(
120 error.message,
121 'Failed to clean up the desktop Clerk bridge for state directory "/tmp/t3-state" (development: false).',
122 );
123 }
124 });
125 });
126
72127 it.each([
73128 { isDevelopment: true, scheme: "t3code-dev" },
74129 { isDevelopment: false, scheme: "t3code" },
apps/desktop/src/app/DesktopClerk.ts+47-3
......@@ -4,6 +4,7 @@ import * as Context from "effect/Context";
44import * as Effect from "effect/Effect";
55import * as Layer from "effect/Layer";
66import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
78import * as Scope from "effect/Scope";
89
910import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";
......@@ -14,6 +15,32 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts";
1415
1516declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;
1617
18export class DesktopClerkBridgeInitializationError extends Schema.TaggedErrorClass<DesktopClerkBridgeInitializationError>()(
19 "DesktopClerkBridgeInitializationError",
20 {
21 stateDir: Schema.String,
22 isDevelopment: Schema.Boolean,
23 cause: Schema.Defect(),
24 },
25) {
26 override get message(): string {
27 return `Failed to initialize the desktop Clerk bridge for state directory "${this.stateDir}" (development: ${this.isDevelopment}).`;
28 }
29}
30
31export class DesktopClerkBridgeCleanupError extends Schema.TaggedErrorClass<DesktopClerkBridgeCleanupError>()(
32 "DesktopClerkBridgeCleanupError",
33 {
34 stateDir: Schema.String,
35 isDevelopment: Schema.Boolean,
36 cause: Schema.Defect(),
37 },
38) {
39 override get message(): string {
40 return `Failed to clean up the desktop Clerk bridge for state directory "${this.stateDir}" (development: ${this.isDevelopment}).`;
41 }
42}
43
1744export class DesktopClerk extends Context.Service<
1845 DesktopClerk,
1946 {
......@@ -55,11 +82,28 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea
5582 });
5683}
5784
58const make = Effect.gen(function* () {
85export const make = Effect.gen(function* () {
5986 const environment = yield* DesktopEnvironment.DesktopEnvironment;
6087 yield* Effect.acquireRelease(
61 Effect.sync(() => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment)),
62 (bridge) => Effect.sync(() => bridge.cleanup()),
88 Effect.try({
89 try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment),
90 catch: (cause) =>
91 new DesktopClerkBridgeInitializationError({
92 stateDir: environment.stateDir,
93 isDevelopment: environment.isDevelopment,
94 cause,
95 }),
96 }),
97 (bridge) =>
98 Effect.try({
99 try: () => bridge.cleanup(),
100 catch: (cause) =>
101 new DesktopClerkBridgeCleanupError({
102 stateDir: environment.stateDir,
103 isDevelopment: environment.isDevelopment,
104 cause,
105 }),
106 }).pipe(Effect.orDie),
63107 );
64108
65109 return DesktopClerk.of({