authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:07:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:07:35-07:00
log9d5ca2cb7ceec88e392d5dc3a70a5979953e4e3e
tree6cb7f503d2576219e261005862781238deb1655d
parentac77fe452bc5c69acbbd05b55029b26ec40d3b43
signature Signed by PGP key B5690EEEBB952194

[codex] Structure Electron protocol teardown failures (#3310)

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

2 files changed, 76 insertions(+), 4 deletions(-)

apps/desktop/src/electron/ElectronProtocol.test.ts+55
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import { beforeEach, vi } from "vite-plus/test";4import { beforeEach, vi } from "vite-plus/test";
45
...@@ -98,6 +99,60 @@ describe("ElectronProtocol", () => {...@@ -98,6 +99,60 @@ describe("ElectronProtocol", () => {
98 }).pipe(Effect.provide(ElectronProtocol.layer)),99 }).pipe(Effect.provide(ElectronProtocol.layer)),
99 );100 );
100101
102 it.effect("preserves protocol registration failures", () =>
103 Effect.gen(function* () {
104 const cause = new Error("protocol registration failed");
105 handleMock.mockImplementationOnce(() => {
106 throw cause;
107 });
108
109 const protocol = yield* ElectronProtocol.ElectronProtocol;
110 const error = yield* Effect.scoped(
111 protocol.registerDesktopProtocol({
112 scheme: "t3code-dev",
113 targetOrigin: new URL("http://127.0.0.1:3773/"),
114 backendOrigin: new URL("http://127.0.0.1:3774/"),
115 clerkFrontendApiHostname: undefined,
116 }),
117 ).pipe(Effect.flip);
118
119 assert.instanceOf(error, ElectronProtocol.ElectronProtocolRegistrationError);
120 assert.equal(error.scheme, "t3code-dev");
121 assert.strictEqual(error.cause, cause);
122 assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".');
123 }).pipe(Effect.provide(ElectronProtocol.layer)),
124 );
125
126 it.effect("preserves protocol unregistration failures", () =>
127 Effect.gen(function* () {
128 const cause = new Error("protocol unregistration failed");
129 unhandleMock.mockImplementationOnce(() => {
130 throw cause;
131 });
132
133 const protocol = yield* ElectronProtocol.ElectronProtocol;
134 const exit = yield* Effect.exit(
135 Effect.scoped(
136 protocol.registerDesktopProtocol({
137 scheme: "t3code",
138 targetOrigin: new URL("http://127.0.0.1:3773/"),
139 backendOrigin: new URL("http://127.0.0.1:3773/"),
140 clerkFrontendApiHostname: undefined,
141 }),
142 ),
143 );
144
145 assert.equal(exit._tag, "Failure");
146 if (exit._tag === "Failure") {
147 const error = Cause.squash(exit.cause);
148 assert.instanceOf(error, ElectronProtocol.ElectronProtocolUnregistrationError);
149 assert.equal(error.scheme, "t3code");
150 assert.strictEqual(error.cause, cause);
151 assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".');
152 }
153 }).pipe(Effect.provide(ElectronProtocol.layer)),
154 );
155
101 it("keeps executable sources host-restricted while allowing runtime network resources", () => {156 it("keeps executable sources host-restricted while allowing runtime network resources", () => {
102 const policy = ElectronProtocol.makeDesktopContentSecurityPolicy({157 const policy = ElectronProtocol.makeDesktopContentSecurityPolicy({
103 scheme: "t3code",158 scheme: "t3code",
apps/desktop/src/electron/ElectronProtocol.ts+21-4
...@@ -31,7 +31,19 @@ export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass<E...@@ -31,7 +31,19 @@ export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass<E
31 },31 },
32) {32) {
33 override get message(): string {33 override get message(): string {
34 return `Failed to register ${this.scheme}: protocol.`;34 return `Failed to register Electron protocol scheme "${this.scheme}".`;
35 }
36}
37
38export class ElectronProtocolUnregistrationError extends Schema.TaggedErrorClass<ElectronProtocolUnregistrationError>()(
39 "ElectronProtocolUnregistrationError",
40 {
41 scheme: Schema.String,
42 cause: Schema.Defect(),
43 },
44) {
45 override get message(): string {
46 return `Failed to unregister Electron protocol scheme "${this.scheme}".`;
35 }47 }
36}48}
3749
...@@ -133,9 +145,14 @@ export const make = Effect.gen(function* () {...@@ -133,9 +145,14 @@ export const make = Effect.gen(function* () {
133 catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }),145 catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }),
134 }).pipe(Effect.andThen(Ref.set(registered, true))),146 }).pipe(Effect.andThen(Ref.set(registered, true))),
135 () =>147 () =>
136 Effect.sync(() => {148 Effect.try({
137 Electron.protocol.unhandle(input.scheme);149 try: () => Electron.protocol.unhandle(input.scheme),
138 }).pipe(Effect.andThen(Ref.set(registered, false))),150 catch: (cause) =>
151 new ElectronProtocolUnregistrationError({
152 scheme: input.scheme,
153 cause,
154 }),
155 }).pipe(Effect.andThen(Ref.set(registered, false)), Effect.orDie),
139 );156 );
140 },157 },
141 );158 );