From 3d9aecf49792a3bd363190f05fa3d419cd59a227 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 14:48:17 +0000 Subject: [PATCH 1/2] test(desktop): reproduce stale t3 launcher with Local environment off (#31507) These tests reproduce #31507: with Local environment off, startup never refreshes /bin/t3, so after an AppImage update the launcher keeps naming the removed AppImage. Shares the existing DesktopApp stubs through a helper so the new case only differs in settings, environment and filesystem. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/app/DesktopApp.test.ts | 360 ++++++++++++++---------- 1 file changed, 218 insertions(+), 142 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.test.ts b/apps/desktop/src/app/DesktopApp.test.ts index 66fc83adae194..235b8839a73f6 100644 --- a/apps/desktop/src/app/DesktopApp.test.ts +++ b/apps/desktop/src/app/DesktopApp.test.ts @@ -1,5 +1,6 @@ import { assert, it } from "@effect/vitest"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; +import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -52,6 +53,7 @@ import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts"; import * as BrowserImport from "../preview/BrowserImport/BrowserImport.ts"; import * as DesktopBackendConfiguration from "../backend/DesktopBackendConfiguration.ts"; import * as DesktopCliCommand from "./DesktopCliCommand.ts"; +import * as DesktopCliShim from "./DesktopCliShim.ts"; import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts"; import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts"; import * as DesktopIpc from "../ipc/DesktopIpc.ts"; @@ -69,6 +71,160 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as MacPermissions from "../permissions/MacPermissions.ts"; import * as PreviewManager from "../preview/Manager.ts"; +interface StubbedDesktop { + readonly localEnvironmentEnabled: boolean; + readonly environment: Partial; + readonly fileSystem: Layer.Layer; + readonly record: (event: string) => Effect.Effect; + readonly windowStarted: Deferred.Deferred; + readonly registerLinuxUrlHandler: Effect.Effect; +} + +const runStubbedDesktop = (stub: StubbedDesktop) => + Effect.gen(function* () { + const quitting = yield* Ref.make(false); + const runApp = DesktopApp.program + .pipe( + Effect.provide(NodeCrypto.layer), + Effect.provideService(DesktopAppIdentity.DesktopAppIdentity, { + resolveUserDataPath: Effect.succeed("/isolated/userdata"), + configure: stub.record("identity"), + } as DesktopAppIdentity.DesktopAppIdentity["Service"]), + Effect.provideService(ElectronApp.ElectronApp, { + setPath: () => Effect.void, + whenReady: stub.record("ready"), + } as unknown as ElectronApp.ElectronApp["Service"]), + Effect.provideService(ElectronDialog.ElectronDialog, { + showErrorBox: () => Effect.die("unexpected fatal startup error"), + } as unknown as ElectronDialog.ElectronDialog["Service"]), + Effect.provideService(DesktopEnvironment.DesktopEnvironment, { + platform: "linux", + isDevelopment: false, + isPackaged: true, + configuredBackendPort: Option.some(3773), + defaultDesktopSettings: { serverExposureMode: "local-only" }, + ...stub.environment, + } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]), + Effect.provideService(DesktopPreReadyPlatform.DesktopPreReadyElectronOptions, { + linux: null, + linuxPasswordStoreCommandLine: "basic", + }), + Effect.provideService(DesktopShellEnvironment.DesktopShellEnvironment, { + installIntoProcess: Effect.succeed(true), + awaitDiscovery: Effect.succeed(true), + updates: Stream.empty, + }), + Effect.provideService(DesktopAppSettings.DesktopAppSettings, { + load: Effect.void, + get: Effect.succeed({ + localEnvironmentEnabled: stub.localEnvironmentEnabled, + serverExposureMode: "local-only", + }), + } as unknown as DesktopAppSettings.DesktopAppSettings["Service"]), + Effect.provideService(DesktopLifecycle.DesktopLifecycle, { + register: Effect.void, + relaunch: () => Effect.void, + }), + Effect.provideService(DesktopClerk.DesktopClerk, { + configure: Effect.void, + } as DesktopClerk.DesktopClerk["Service"]), + Effect.provideService(ElectronSafeStorage.ElectronSafeStorage, { + selectedStorageBackend: Effect.succeedNone, + } as unknown as ElectronSafeStorage.ElectronSafeStorage["Service"]), + Effect.provideService(PreviewPasskeys.PreviewPasskeys, { + configure: Effect.void, + } as PreviewPasskeys.PreviewPasskeys["Service"]), + Effect.provideService(DesktopApplicationMenu.DesktopApplicationMenu, { + configure: Effect.void, + }), + Effect.provideService(DesktopUpdates.DesktopUpdates, { + configure: Effect.void, + } as unknown as DesktopUpdates.DesktopUpdates["Service"]), + Effect.provideService(DesktopLinuxUrlHandler.DesktopLinuxUrlHandler, { + register: stub.registerLinuxUrlHandler, + }), + ) + .pipe( + Effect.provideService(ElectronProtocol.ElectronProtocol, { + registerDesktopProtocol: () => Effect.void, + } as unknown as ElectronProtocol.ElectronProtocol["Service"]), + Effect.provideService(DesktopLegacyLocalStorage.DesktopLegacyLocalStorage, { + load: () => Effect.void, + } as unknown as DesktopLegacyLocalStorage.DesktopLegacyLocalStorage["Service"]), + Effect.provideService(DesktopSnapShot.DesktopSnapShot, { + initialize: Effect.void, + } as DesktopSnapShot.DesktopSnapShot["Service"]), + Effect.provideService(DesktopState.DesktopState, { + quitting, + } as DesktopState.DesktopState["Service"]), + Effect.provideService(DesktopWindow.DesktopWindow, { + ensureMain: stub + .record("window") + .pipe(Effect.andThen(Deferred.succeed(stub.windowStarted, undefined))), + createMainIfBackendReady: stub + .record("window") + .pipe(Effect.andThen(Deferred.succeed(stub.windowStarted, undefined))), + } as unknown as DesktopWindow.DesktopWindow["Service"]), + Effect.provideService(DesktopBackendPool.DesktopBackendPool, { + primary: Effect.succeed({ + start: stub.record("backend"), + currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }), + }), + list: Effect.succeed([]), + } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]), + Effect.provideService(DesktopServerExposure.DesktopServerExposure, { + configureFromSettings: () => Effect.succeed({ mode: "local-only" }), + backendConfig: Effect.succeed({ httpBaseUrl: new URL("http://127.0.0.1:3773") }), + } as unknown as DesktopServerExposure.DesktopServerExposure["Service"]), + Effect.provideService(DesktopWslBackend.DesktopWslBackend, { + reconcile: Effect.void, + } as DesktopWslBackend.DesktopWslBackend["Service"]), + Effect.provideService(DesktopAppActivation.DesktopAppActivation, { + start: Effect.void, + } as unknown as DesktopAppActivation.DesktopAppActivation["Service"]), + Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, { + shutdown: Effect.void, + } as DesktopRendererHistory.DesktopRendererHistory["Service"]), + ); + return yield* runApp.pipe( + Effect.provide( + Layer.mergeAll( + Layer.mock(BrowserImport.BrowserImport, {}), + Layer.mock(DesktopBackendConfiguration.DesktopBackendConfiguration, {}), + Layer.mock(DesktopCliCommand.DesktopCliCommand, {}), + Layer.mock(DesktopClientSettings.DesktopClientSettings, {}), + Layer.mock(DesktopConnectionCatalogStore.DesktopConnectionCatalogStore, {}), + Layer.mock(DesktopIpc.DesktopIpc, {}), + Layer.mock(DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth, {}), + Layer.mock(DesktopManagedPolicy.DesktopManagedPolicy, {}), + Layer.mock(DesktopSshEnvironment.DesktopSshEnvironment, {}), + Layer.mock(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {}), + Layer.mock(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {}), + Layer.mock(DesktopWebLinks.DesktopWebLinks, {}), + Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {}), + Layer.mock(ElectronMenu.ElectronMenu, {}), + Layer.mock(ElectronShell.ElectronShell, {}), + Layer.mock(ElectronTheme.ElectronTheme, {}), + Layer.mock(ElectronWindow.ElectronWindow, {}), + Layer.mock(MacPermissions.MacPermissions, {}), + Layer.mock(PreviewManager.PreviewManager, { + isBrowserPartition: () => { + throw new Error("unexpected browser partition lookup"); + }, + }), + stub.fileSystem, + Layer.mock(NetService.NetService, {}), + Path.layer, + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make(() => Effect.die("unexpected HTTP request")), + ), + ), + ), + Effect.forkScoped, + ); + }); + it.effect( "starts the backend and window while Linux registration is stalled and cancels it on shutdown", () => @@ -77,151 +233,21 @@ it.effect( const releaseRegistration = yield* Deferred.make(); const registrationStopped = yield* Deferred.make(); const windowStarted = yield* Deferred.make(); - const quitting = yield* Ref.make(false); const events: string[] = []; - const record = (event: string) => - Effect.sync(() => { - events.push(event); - }); - const runApp = DesktopApp.program - .pipe( - Effect.provide(NodeCrypto.layer), - Effect.provideService(DesktopAppIdentity.DesktopAppIdentity, { - resolveUserDataPath: Effect.succeed("/isolated/userdata"), - configure: record("identity"), - } as DesktopAppIdentity.DesktopAppIdentity["Service"]), - Effect.provideService(ElectronApp.ElectronApp, { - setPath: () => Effect.void, - whenReady: record("ready"), - } as unknown as ElectronApp.ElectronApp["Service"]), - Effect.provideService(ElectronDialog.ElectronDialog, { - showErrorBox: () => Effect.die("unexpected fatal startup error"), - } as unknown as ElectronDialog.ElectronDialog["Service"]), - Effect.provideService(DesktopEnvironment.DesktopEnvironment, { - platform: "linux", - isDevelopment: false, - isPackaged: true, - configuredBackendPort: Option.some(3773), - defaultDesktopSettings: { serverExposureMode: "local-only" }, - } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]), - Effect.provideService(DesktopPreReadyPlatform.DesktopPreReadyElectronOptions, { - linux: null, - linuxPasswordStoreCommandLine: "basic", - }), - Effect.provideService(DesktopShellEnvironment.DesktopShellEnvironment, { - installIntoProcess: Effect.succeed(true), - awaitDiscovery: Effect.succeed(true), - updates: Stream.empty, + const app = yield* runStubbedDesktop({ + localEnvironmentEnabled: true, + environment: {}, + fileSystem: FileSystem.layerNoop({}), + record: (event) => + Effect.sync(() => { + events.push(event); }), - Effect.provideService(DesktopAppSettings.DesktopAppSettings, { - load: Effect.void, - get: Effect.succeed({ - localEnvironmentEnabled: true, - serverExposureMode: "local-only", - }), - } as unknown as DesktopAppSettings.DesktopAppSettings["Service"]), - Effect.provideService(DesktopLifecycle.DesktopLifecycle, { - register: Effect.void, - relaunch: () => Effect.void, - }), - Effect.provideService(DesktopClerk.DesktopClerk, { - configure: Effect.void, - } as DesktopClerk.DesktopClerk["Service"]), - Effect.provideService(ElectronSafeStorage.ElectronSafeStorage, { - selectedStorageBackend: Effect.succeedNone, - } as unknown as ElectronSafeStorage.ElectronSafeStorage["Service"]), - Effect.provideService(PreviewPasskeys.PreviewPasskeys, { - configure: Effect.void, - } as PreviewPasskeys.PreviewPasskeys["Service"]), - Effect.provideService(DesktopApplicationMenu.DesktopApplicationMenu, { - configure: Effect.void, - }), - Effect.provideService(DesktopUpdates.DesktopUpdates, { - configure: Effect.void, - } as unknown as DesktopUpdates.DesktopUpdates["Service"]), - Effect.provideService(DesktopLinuxUrlHandler.DesktopLinuxUrlHandler, { - register: Deferred.succeed(registrationStarted, undefined).pipe( - Effect.andThen(Deferred.await(releaseRegistration)), - Effect.ensuring(Deferred.succeed(registrationStopped, undefined)), - ), - }), - ) - .pipe( - Effect.provideService(ElectronProtocol.ElectronProtocol, { - registerDesktopProtocol: () => Effect.void, - } as unknown as ElectronProtocol.ElectronProtocol["Service"]), - Effect.provideService(DesktopLegacyLocalStorage.DesktopLegacyLocalStorage, { - load: () => Effect.void, - } as unknown as DesktopLegacyLocalStorage.DesktopLegacyLocalStorage["Service"]), - Effect.provideService(DesktopSnapShot.DesktopSnapShot, { - initialize: Effect.void, - } as DesktopSnapShot.DesktopSnapShot["Service"]), - Effect.provideService(DesktopState.DesktopState, { - quitting, - } as DesktopState.DesktopState["Service"]), - Effect.provideService(DesktopWindow.DesktopWindow, { - ensureMain: record("window").pipe( - Effect.andThen(Deferred.succeed(windowStarted, undefined)), - ), - } as unknown as DesktopWindow.DesktopWindow["Service"]), - Effect.provideService(DesktopBackendPool.DesktopBackendPool, { - primary: Effect.succeed({ - start: record("backend"), - currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }), - }), - list: Effect.succeed([]), - } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]), - Effect.provideService(DesktopServerExposure.DesktopServerExposure, { - configureFromSettings: () => Effect.succeed({ mode: "local-only" }), - backendConfig: Effect.succeed({ httpBaseUrl: new URL("http://127.0.0.1:3773") }), - } as unknown as DesktopServerExposure.DesktopServerExposure["Service"]), - Effect.provideService(DesktopWslBackend.DesktopWslBackend, { - reconcile: Effect.void, - } as DesktopWslBackend.DesktopWslBackend["Service"]), - Effect.provideService(DesktopAppActivation.DesktopAppActivation, { - start: Effect.void, - } as unknown as DesktopAppActivation.DesktopAppActivation["Service"]), - Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, { - shutdown: Effect.void, - } as DesktopRendererHistory.DesktopRendererHistory["Service"]), - ); - const app = yield* runApp.pipe( - Effect.provide( - Layer.mergeAll( - Layer.mock(BrowserImport.BrowserImport, {}), - Layer.mock(DesktopBackendConfiguration.DesktopBackendConfiguration, {}), - Layer.mock(DesktopCliCommand.DesktopCliCommand, {}), - Layer.mock(DesktopClientSettings.DesktopClientSettings, {}), - Layer.mock(DesktopConnectionCatalogStore.DesktopConnectionCatalogStore, {}), - Layer.mock(DesktopIpc.DesktopIpc, {}), - Layer.mock(DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth, {}), - Layer.mock(DesktopManagedPolicy.DesktopManagedPolicy, {}), - Layer.mock(DesktopSshEnvironment.DesktopSshEnvironment, {}), - Layer.mock(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {}), - Layer.mock(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {}), - Layer.mock(DesktopWebLinks.DesktopWebLinks, {}), - Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {}), - Layer.mock(ElectronMenu.ElectronMenu, {}), - Layer.mock(ElectronShell.ElectronShell, {}), - Layer.mock(ElectronTheme.ElectronTheme, {}), - Layer.mock(ElectronWindow.ElectronWindow, {}), - Layer.mock(MacPermissions.MacPermissions, {}), - Layer.mock(PreviewManager.PreviewManager, { - isBrowserPartition: () => { - throw new Error("unexpected browser partition lookup"); - }, - }), - FileSystem.layerNoop({}), - Layer.mock(NetService.NetService, {}), - Path.layer, - Layer.succeed( - HttpClient.HttpClient, - HttpClient.make(() => Effect.die("unexpected HTTP request")), - ), - ), + windowStarted, + registerLinuxUrlHandler: Deferred.succeed(registrationStarted, undefined).pipe( + Effect.andThen(Deferred.await(releaseRegistration)), + Effect.ensuring(Deferred.succeed(registrationStopped, undefined)), ), - Effect.forkScoped, - ); + }); yield* Deferred.await(registrationStarted); yield* Deferred.await(windowStarted); assert.deepEqual(events, ["identity", "ready", "identity", "backend", "window"]); @@ -231,3 +257,53 @@ it.effect( assert.isTrue(yield* Deferred.isDone(registrationStopped)); }).pipe(Effect.provide(DesktopShutdown.layer)), ); + +it.effect("points a stale t3 launcher at the updated AppImage with Local environment off", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-desktop-app-" }); + const launcher = DesktopCliShim.launcherPath({ + path, + baseDir, + platform: "linux", + } as DesktopEnvironment.DesktopEnvironment["Service"]); + const removedAppImage = path.join(baseDir, "T3-Code-0.1.6-x86_64.AppImage"); + const updatedAppImage = path.join(baseDir, "T3-Code-0.1.7-x86_64.AppImage"); + yield* fs.makeDirectory(path.dirname(launcher), { recursive: true }); + yield* fs.writeFileString( + launcher, + DesktopCliShim.renderCliShim({ + target: { kind: "appimage", appImage: removedAppImage, executableName: "t3code" }, + shimPath: launcher, + t3Home: baseDir, + }), + ); + yield* fs.writeFileString(updatedAppImage, "", { mode: 0o755 }); + const windowStarted = yield* Deferred.make(); + + const app = yield* runStubbedDesktop({ + localEnvironmentEnabled: false, + environment: { + path, + baseDir, + serverRoot: path.join(baseDir, "resources", "app.asar"), + appImagePath: Option.some(updatedAppImage), + }, + fileSystem: Layer.succeed(FileSystem.FileSystem, fs), + record: () => Effect.void, + windowStarted, + registerLinuxUrlHandler: Effect.void, + }); + yield* Deferred.await(windowStarted); + yield* (yield* DesktopShutdown.DesktopShutdown).request; + yield* Fiber.join(app); + + const content = yield* fs.readFileString(launcher); + assert.include( + content, + `app='${updatedAppImage}'`, + "startup left the t3 launcher pointing at the removed AppImage", + ); + }).pipe(Effect.provide(Layer.mergeAll(DesktopShutdown.layer, NodeServices.layer))), +); From 33ca0d4919a091eac06dd63c4cd7c3627750b412 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 14:51:03 +0000 Subject: [PATCH 2/2] fix(desktop): opening the app refreshes the t3 launcher with Local environment off With Local environment off, startup returned before the primary backend config ran, which is the only startup path that rewrites /bin/t3. After an AppImage update deleted the old image, `t3` kept reporting that T3 Code had moved. Startup now installs the launcher in that branch too. Fixes pingdotgg/t3code#31507 Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/app/DesktopApp.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 151c227e47ff4..49496298e16a0 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -16,6 +16,7 @@ import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts"; import * as DesktopAppActivation from "./DesktopAppActivation.ts"; import * as DesktopAppIdentity from "./DesktopAppIdentity.ts"; import * as DesktopClerk from "./DesktopClerk.ts"; +import * as DesktopCliShim from "./DesktopCliShim.ts"; import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts"; import * as DesktopWindow from "../window/DesktopWindow.ts"; import * as PreviewPasskeys from "../preview/Passkeys.ts"; @@ -195,6 +196,8 @@ const bootstrap = Effect.gen(function* () { if (!settings.localEnvironmentEnabled) { yield* logBootstrapInfo("bootstrap skipping local environment (disabled in settings)"); + // The backend config refreshes the launcher when the local environment runs; an update may move the app. + yield* DesktopCliShim.install; if (!(yield* Ref.get(state.quitting))) { yield* desktopWindow.createMainIfBackendReady; }