| 1 | import * as NodePath from "@effect/platform-node/NodePath"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { assert, describe, it } from "@effect/vitest"; |
| 4 | import { vi } from "vite-plus/test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Option from "effect/Option"; |
| 8 | |
| 9 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 10 | import * as DesktopConfig from "./DesktopConfig.ts"; |
| 11 | |
| 12 | const defaultInput = { |
| 13 | dirname: "/repo/apps/desktop/dist-electron", |
| 14 | homeDirectory: "/Users/alice", |
| 15 | platform: "darwin", |
| 16 | processArch: "arm64", |
| 17 | appVersion: "0.0.22", |
| 18 | appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar", |
| 19 | isPackaged: false, |
| 20 | resourcesPath: "/Applications/T3 Code.app/Contents/Resources", |
| 21 | runningUnderArm64Translation: false, |
| 22 | } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; |
| 23 | |
| 24 | const layerEnvironment = ( |
| 25 | overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {}, |
| 26 | env: Record<string, string | undefined> = {}, |
| 27 | ) => |
| 28 | DesktopEnvironment.layer({ |
| 29 | ...defaultInput, |
| 30 | ...overrides, |
| 31 | }).pipe( |
| 32 | Layer.provide( |
| 33 | Layer.mergeAll(NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest(env)), |
| 34 | ), |
| 35 | ); |
| 36 | |
| 37 | const makeEnvironment = ( |
| 38 | overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {}, |
| 39 | env: Record<string, string | undefined> = {}, |
| 40 | ) => DesktopEnvironment.DesktopEnvironment.pipe(Effect.provide(layerEnvironment(overrides, env))); |
| 41 | |
| 42 | describe("DesktopEnvironment", () => { |
| 43 | it.effect("derives state paths and development identity inside Effect", () => |
| 44 | Effect.gen(function* () { |
| 45 | const environment = yield* makeEnvironment( |
| 46 | {}, |
| 47 | { |
| 48 | T3CODE_HOME: " /tmp/t3 ", |
| 49 | T3CODE_COMMIT_HASH: " 0123456789abcdef ", |
| 50 | T3CODE_PORT: "4949", |
| 51 | VITE_DEV_SERVER_URL: "http://localhost:5173", |
| 52 | T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH: " /remote/server.mjs ", |
| 53 | T3CODE_OTLP_TRACES_URL: " http://127.0.0.1:4318/v1/traces ", |
| 54 | T3CODE_OTLP_METRICS_URL: " http://127.0.0.1:4318/v1/metrics ", |
| 55 | T3CODE_OTLP_LOGS_URL: " http://127.0.0.1:4318/v1/logs ", |
| 56 | T3CODE_OTLP_EXPORT_INTERVAL_MS: "2500", |
| 57 | T3CODE_OTLP_HEADERS: "authorization=Basic%20abc%3D%3D,x-tenant=t3", |
| 58 | T3CODE_OTLP_PROTOCOL: "http/protobuf", |
| 59 | }, |
| 60 | ); |
| 61 | |
| 62 | assert.equal(environment.isDevelopment, true); |
| 63 | assert.equal(environment.appDataDirectory, "/Users/alice/Library/Application Support"); |
| 64 | assert.equal(environment.baseDir, "/tmp/t3"); |
| 65 | assert.equal(environment.stateDir, "/tmp/t3/userdata"); |
| 66 | assert.equal(environment.desktopSettingsPath, "/tmp/t3/userdata/desktop-settings.json"); |
| 67 | assert.equal(environment.clientSettingsPath, "/tmp/t3/userdata/client-settings.json"); |
| 68 | assert.equal( |
| 69 | environment.savedEnvironmentRegistryPath, |
| 70 | "/tmp/t3/userdata/saved-environments.json", |
| 71 | ); |
| 72 | assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json"); |
| 73 | assert.equal(environment.logDir, "/tmp/t3/userdata/logs"); |
| 74 | assert.equal(environment.browserArtifactsDir, "/tmp/t3/userdata/browser-artifacts"); |
| 75 | assert.equal(environment.rootDir, "/repo"); |
| 76 | assert.equal(environment.appRoot, "/repo"); |
| 77 | assert.equal(environment.serverRoot, "/repo"); |
| 78 | assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs"); |
| 79 | assert.equal(environment.backendCwd, "/repo"); |
| 80 | assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev"); |
| 81 | assert.equal(environment.linuxWmClass, "t3code-dev"); |
| 82 | assert.equal(environment.linuxDesktopEntryName, "com.t3tools.T3Code.Development.desktop"); |
| 83 | assert.deepEqual( |
| 84 | Option.map(environment.devServerUrl, (url) => url.href), |
| 85 | Option.some("http://localhost:5173/"), |
| 86 | ); |
| 87 | assert.deepEqual(environment.devRemoteT3ServerEntryPath, Option.some("/remote/server.mjs")); |
| 88 | assert.deepEqual(environment.configuredBackendPort, Option.some(4949)); |
| 89 | assert.deepEqual(environment.commitHashOverride, Option.some("0123456789abcdef")); |
| 90 | assert.deepEqual(environment.otlpTracesUrl, Option.some("http://127.0.0.1:4318/v1/traces")); |
| 91 | assert.deepEqual(environment.otlpMetricsUrl, Option.some("http://127.0.0.1:4318/v1/metrics")); |
| 92 | assert.deepEqual(environment.otlpLogsUrl, Option.some("http://127.0.0.1:4318/v1/logs")); |
| 93 | assert.equal(environment.otlpExportIntervalMs, 2500); |
| 94 | assert.deepEqual( |
| 95 | environment.otlpHeaders, |
| 96 | Option.some({ |
| 97 | authorization: "Basic abc==", |
| 98 | "x-tenant": "t3", |
| 99 | }), |
| 100 | ); |
| 101 | assert.equal(environment.otlpProtocol, "http/protobuf"); |
| 102 | }), |
| 103 | ); |
| 104 | |
| 105 | it.effect("stores production state under userdata in an explicit home", () => |
| 106 | Effect.gen(function* () { |
| 107 | const environment = yield* makeEnvironment( |
| 108 | {}, |
| 109 | { |
| 110 | T3CODE_HOME: "/tmp/t3", |
| 111 | }, |
| 112 | ); |
| 113 | |
| 114 | assert.equal(environment.isDevelopment, false); |
| 115 | assert.equal(environment.stateDir, "/tmp/t3/userdata"); |
| 116 | assert.equal(environment.logDir, "/tmp/t3/userdata/logs"); |
| 117 | assert.equal(environment.browserArtifactsDir, "/tmp/t3/userdata/browser-artifacts"); |
| 118 | assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json"); |
| 119 | assert.equal(environment.otlpProtocol, "http/json"); |
| 120 | }), |
| 121 | ); |
| 122 | |
| 123 | it.effect("uses the packaged Windows server sidecar as the backend root", () => |
| 124 | Effect.gen(function* () { |
| 125 | const environment = yield* makeEnvironment({ |
| 126 | platform: "win32", |
| 127 | isPackaged: true, |
| 128 | appPath: "/install/resources/app.asar", |
| 129 | resourcesPath: "/install/resources", |
| 130 | }); |
| 131 | |
| 132 | assert.equal(environment.appRoot, "/install/resources/app.asar"); |
| 133 | assert.equal(environment.serverRoot, "/install/resources/server.asar"); |
| 134 | assert.equal( |
| 135 | environment.backendEntryPath, |
| 136 | "/install/resources/server.asar/apps/server/dist/bin.mjs", |
| 137 | ); |
| 138 | assert.equal( |
| 139 | environment.clientAssetsDir, |
| 140 | "/install/resources/server.asar/apps/server/dist/client", |
| 141 | ); |
| 142 | }), |
| 143 | ); |
| 144 | |
| 145 | it.effect("keeps downloaded Windows desktop and server dependencies separate", () => |
| 146 | Effect.gen(function* () { |
| 147 | vi.stubEnv("T3CODE_DESKTOP_PAYLOAD", JSON.stringify({ directory: "/updates/release-new" })); |
| 148 | try { |
| 149 | const environment = yield* makeEnvironment({ |
| 150 | platform: "win32", |
| 151 | isPackaged: true, |
| 152 | appPath: "/updates/release-new", |
| 153 | resourcesPath: "/install/resources", |
| 154 | }); |
| 155 | assert.equal(environment.serverRoot, "/updates/release-new/server"); |
| 156 | assert.equal( |
| 157 | environment.backendEntryPath, |
| 158 | "/updates/release-new/server/apps/server/dist/bin.mjs", |
| 159 | ); |
| 160 | assert.equal( |
| 161 | environment.clientAssetsDir, |
| 162 | "/updates/release-new/server/apps/server/dist/client", |
| 163 | ); |
| 164 | } finally { |
| 165 | vi.unstubAllEnvs(); |
| 166 | } |
| 167 | }), |
| 168 | ); |
| 169 | |
| 170 | it.effect("uses the stable desktop entry as the packaged Linux portal identity", () => |
| 171 | Effect.gen(function* () { |
| 172 | const environment = yield* makeEnvironment({ |
| 173 | platform: "linux", |
| 174 | isPackaged: true, |
| 175 | appPath: "/tmp/.mount_t3code/resources/app.asar", |
| 176 | resourcesPath: "/tmp/.mount_t3code/resources", |
| 177 | }); |
| 178 | |
| 179 | assert.equal(environment.linuxDesktopEntryName, "com.t3tools.T3Code.desktop"); |
| 180 | }), |
| 181 | ); |
| 182 | |
| 183 | it.effect("keeps implicit development state separate from production state", () => |
| 184 | Effect.gen(function* () { |
| 185 | const development = yield* makeEnvironment( |
| 186 | {}, |
| 187 | { VITE_DEV_SERVER_URL: "http://localhost:5173" }, |
| 188 | ); |
| 189 | const production = yield* makeEnvironment(); |
| 190 | |
| 191 | assert.equal(development.stateDir, "/Users/alice/.t3/dev"); |
| 192 | assert.equal(production.stateDir, "/Users/alice/.t3/userdata"); |
| 193 | }), |
| 194 | ); |
| 195 | |
| 196 | it.effect("uses a configured app user model id override", () => |
| 197 | Effect.gen(function* () { |
| 198 | const environment = yield* makeEnvironment( |
| 199 | {}, |
| 200 | { |
| 201 | T3CODE_DESKTOP_APP_USER_MODEL_ID: " com.t3tools.t3code.dev.local ", |
| 202 | VITE_DEV_SERVER_URL: "http://localhost:5173", |
| 203 | }, |
| 204 | ); |
| 205 | |
| 206 | assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev.local"); |
| 207 | }), |
| 208 | ); |
| 209 | |
| 210 | it.effect("resolves picker defaults without nullish sentinels", () => |
| 211 | Effect.gen(function* () { |
| 212 | const environment = yield* makeEnvironment(); |
| 213 | |
| 214 | assert.deepEqual(environment.resolvePickFolderDefaultPath(null), Option.none()); |
| 215 | assert.deepEqual( |
| 216 | environment.resolvePickFolderDefaultPath({ initialPath: " " }), |
| 217 | Option.none(), |
| 218 | ); |
| 219 | assert.deepEqual( |
| 220 | environment.resolvePickFolderDefaultPath({ initialPath: "~" }), |
| 221 | Option.some("/Users/alice"), |
| 222 | ); |
| 223 | assert.deepEqual( |
| 224 | environment.resolvePickFolderDefaultPath({ initialPath: "~/project" }), |
| 225 | Option.some("/Users/alice/project"), |
| 226 | ); |
| 227 | }), |
| 228 | ); |
| 229 | }); |