| 1 | import { |
| 2 | AuthStandardClientScopes, |
| 3 | EnvironmentId, |
| 4 | PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 5 | type DesktopBridge, |
| 6 | type DesktopSshEnvironmentTarget, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import { describe, expect, it } from "@effect/vitest"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import { |
| 11 | ConnectionBlockedError, |
| 12 | ConnectionTransientError, |
| 13 | } from "@t3tools/client-runtime/connection"; |
| 14 | |
| 15 | import { |
| 16 | canRetainCachedPlatformRegistrationAfterRefreshFailure, |
| 17 | canReuseCachedPlatformRegistration, |
| 18 | isRejectedBootstrapCredentialError, |
| 19 | isRejectedSecondaryBootstrap, |
| 20 | nextRejectedSecondaryBootstrap, |
| 21 | primaryRegistrationToRetainAfterTopologyRead, |
| 22 | provisionDesktopSshEnvironment, |
| 23 | readPrimaryEnvironmentTargetResult, |
| 24 | secondaryRegistrationsToRetainAfterTopologyRead, |
| 25 | secondaryBearerExpiresAtEpochMs, |
| 26 | secondaryBearerRefreshAtEpochMs, |
| 27 | } from "./platform.ts"; |
| 28 | |
| 29 | const TARGET: DesktopSshEnvironmentTarget = { |
| 30 | alias: "devbox", |
| 31 | hostname: "devbox.example.test", |
| 32 | username: "developer", |
| 33 | port: 22, |
| 34 | }; |
| 35 | |
| 36 | function makeBridge( |
| 37 | calls: string[], |
| 38 | options?: { readonly failDescriptor?: boolean }, |
| 39 | ): DesktopBridge { |
| 40 | return { |
| 41 | ensureSshEnvironment: async (target: DesktopSshEnvironmentTarget) => { |
| 42 | calls.push("ensure"); |
| 43 | return { |
| 44 | target, |
| 45 | httpBaseUrl: "http://127.0.0.1:3201/", |
| 46 | wsBaseUrl: "ws://127.0.0.1:3201/", |
| 47 | pairingToken: "pairing-token", |
| 48 | }; |
| 49 | }, |
| 50 | fetchSshEnvironmentDescriptor: async () => { |
| 51 | calls.push("descriptor"); |
| 52 | if (options?.failDescriptor === true) { |
| 53 | throw new Error("descriptor unavailable"); |
| 54 | } |
| 55 | return { |
| 56 | environmentId: EnvironmentId.make("environment-ssh"), |
| 57 | label: "SSH environment", |
| 58 | platform: { |
| 59 | os: "linux", |
| 60 | arch: "x64", |
| 61 | }, |
| 62 | serverVersion: "0.0.0-test", |
| 63 | capabilities: { |
| 64 | repositoryIdentity: true, |
| 65 | }, |
| 66 | }; |
| 67 | }, |
| 68 | bootstrapSshBearerSession: async () => { |
| 69 | calls.push("token"); |
| 70 | return { |
| 71 | access_token: "bearer-token", |
| 72 | issued_token_type: "urn:ietf:params:oauth:token-type:access_token", |
| 73 | token_type: "Bearer", |
| 74 | expires_in: 3_600, |
| 75 | scope: AuthStandardClientScopes.join(" "), |
| 76 | }; |
| 77 | }, |
| 78 | } as unknown as DesktopBridge; |
| 79 | } |
| 80 | |
| 81 | describe("desktop SSH pairing", () => { |
| 82 | it.effect("fetches the descriptor before consuming the one-time credential", () => |
| 83 | Effect.gen(function* () { |
| 84 | const calls: string[] = []; |
| 85 | |
| 86 | const provisioned = yield* provisionDesktopSshEnvironment(makeBridge(calls), TARGET); |
| 87 | |
| 88 | expect(provisioned.environmentId).toBe(EnvironmentId.make("environment-ssh")); |
| 89 | expect(calls).toEqual(["ensure", "descriptor", "token"]); |
| 90 | }), |
| 91 | ); |
| 92 | |
| 93 | it.effect("does not consume the credential when adding a route to a different machine", () => |
| 94 | Effect.gen(function* () { |
| 95 | const calls: string[] = []; |
| 96 | |
| 97 | const error = yield* provisionDesktopSshEnvironment( |
| 98 | makeBridge(calls), |
| 99 | TARGET, |
| 100 | EnvironmentId.make("some-other-machine"), |
| 101 | ).pipe(Effect.flip); |
| 102 | |
| 103 | expect(error).toMatchObject({ reason: "configuration" }); |
| 104 | expect(calls).toEqual(["ensure", "descriptor"]); |
| 105 | }), |
| 106 | ); |
| 107 | |
| 108 | it.effect("does not consume the credential when descriptor discovery fails", () => |
| 109 | Effect.gen(function* () { |
| 110 | const calls: string[] = []; |
| 111 | |
| 112 | yield* provisionDesktopSshEnvironment( |
| 113 | makeBridge(calls, { failDescriptor: true }), |
| 114 | TARGET, |
| 115 | ).pipe(Effect.flip); |
| 116 | |
| 117 | expect(calls).toEqual(["ensure", "descriptor"]); |
| 118 | }), |
| 119 | ); |
| 120 | }); |
| 121 | |
| 122 | describe("desktop-local bearer cache", () => { |
| 123 | const registration = {} as never; |
| 124 | |
| 125 | it("refreshes a secondary bearer before it expires", () => { |
| 126 | const issuedAtEpochMs = 10_000; |
| 127 | const refreshAtEpochMs = secondaryBearerRefreshAtEpochMs(issuedAtEpochMs, 60); |
| 128 | const expiresAtEpochMs = secondaryBearerExpiresAtEpochMs(issuedAtEpochMs, 60); |
| 129 | const cached = { |
| 130 | expiresAtEpochMs, |
| 131 | signature: "secondary-signature", |
| 132 | registration, |
| 133 | refreshAtEpochMs, |
| 134 | }; |
| 135 | |
| 136 | expect(refreshAtEpochMs).toBe(65_000); |
| 137 | expect(canReuseCachedPlatformRegistration(cached, cached.signature, 64_999)).toBe(true); |
| 138 | expect(canReuseCachedPlatformRegistration(cached, cached.signature, 65_000)).toBe(false); |
| 139 | expect( |
| 140 | canRetainCachedPlatformRegistrationAfterRefreshFailure(cached, cached.signature, 69_999), |
| 141 | ).toBe(true); |
| 142 | expect( |
| 143 | canRetainCachedPlatformRegistrationAfterRefreshFailure(cached, cached.signature, 70_000), |
| 144 | ).toBe(false); |
| 145 | }); |
| 146 | |
| 147 | it("does not cache credentials whose lifetime is shorter than the refresh skew", () => { |
| 148 | const refreshAtEpochMs = secondaryBearerRefreshAtEpochMs(10_000, 3); |
| 149 | const cached = { |
| 150 | expiresAtEpochMs: secondaryBearerExpiresAtEpochMs(10_000, 3), |
| 151 | signature: "secondary-signature", |
| 152 | registration, |
| 153 | refreshAtEpochMs, |
| 154 | }; |
| 155 | |
| 156 | expect(refreshAtEpochMs).toBe(10_000); |
| 157 | expect(canReuseCachedPlatformRegistration(cached, cached.signature, 10_000)).toBe(false); |
| 158 | }); |
| 159 | |
| 160 | it("retains only unexpired secondaries after a topology read failure", () => { |
| 161 | const valid = { |
| 162 | expiresAtEpochMs: 20_000, |
| 163 | signature: "valid-secondary", |
| 164 | registration, |
| 165 | refreshAtEpochMs: 15_000, |
| 166 | }; |
| 167 | const previous = new Map([ |
| 168 | ["valid-secondary", valid], |
| 169 | [ |
| 170 | "expired-secondary", |
| 171 | { |
| 172 | expiresAtEpochMs: 10_000, |
| 173 | signature: "expired-secondary", |
| 174 | registration, |
| 175 | refreshAtEpochMs: 5_000, |
| 176 | }, |
| 177 | ], |
| 178 | ]); |
| 179 | |
| 180 | expect( |
| 181 | secondaryRegistrationsToRetainAfterTopologyRead( |
| 182 | previous, |
| 183 | { _tag: "Failure", cause: new Error("IPC unavailable") }, |
| 184 | 10_000, |
| 185 | ), |
| 186 | ).toEqual(new Map([["valid-secondary", valid]])); |
| 187 | }); |
| 188 | |
| 189 | it("treats a successful empty topology as authoritative removal", () => { |
| 190 | const previous = new Map([ |
| 191 | [ |
| 192 | "secondary", |
| 193 | { |
| 194 | expiresAtEpochMs: 20_000, |
| 195 | signature: "secondary", |
| 196 | registration, |
| 197 | refreshAtEpochMs: 15_000, |
| 198 | }, |
| 199 | ], |
| 200 | ]); |
| 201 | |
| 202 | expect( |
| 203 | secondaryRegistrationsToRetainAfterTopologyRead( |
| 204 | previous, |
| 205 | { _tag: "Success", bootstraps: [] }, |
| 206 | 10_000, |
| 207 | ), |
| 208 | ).toEqual(new Map()); |
| 209 | }); |
| 210 | }); |
| 211 | |
| 212 | describe("rejected desktop-local bootstrap tokens", () => { |
| 213 | it("backs off on a rejected signature and retries it on a growing, capped delay", () => { |
| 214 | const signature = "http://a|ws://a|old-token"; |
| 215 | const first = nextRejectedSecondaryBootstrap(undefined, signature, 0); |
| 216 | |
| 217 | expect(isRejectedSecondaryBootstrap(first, signature, 59_999)).toBe(true); |
| 218 | // A backend restarted on the same port accepts the same token again, so it is retried. |
| 219 | expect(isRejectedSecondaryBootstrap(first, signature, 60_000)).toBe(false); |
| 220 | |
| 221 | const second = nextRejectedSecondaryBootstrap(first, signature, 60_000); |
| 222 | expect(second.retryAtEpochMs).toBe(180_000); |
| 223 | |
| 224 | let backoff = second; |
| 225 | for (let attempt = 0; attempt < 10; attempt += 1) { |
| 226 | backoff = nextRejectedSecondaryBootstrap(backoff, signature, 0); |
| 227 | } |
| 228 | expect(backoff.delayMs).toBe(30 * 60_000); |
| 229 | }); |
| 230 | |
| 231 | it("retries at once when the token or endpoint changes", () => { |
| 232 | const rejected = nextRejectedSecondaryBootstrap(undefined, "http://a|ws://a|old-token", 0); |
| 233 | |
| 234 | expect(isRejectedSecondaryBootstrap(rejected, "http://a|ws://a|new-token", 1)).toBe(false); |
| 235 | expect(isRejectedSecondaryBootstrap(rejected, "http://b|ws://b|old-token", 1)).toBe(false); |
| 236 | expect(isRejectedSecondaryBootstrap(undefined, "http://a|ws://a|old-token", 1)).toBe(false); |
| 237 | expect(nextRejectedSecondaryBootstrap(rejected, "http://a|ws://a|new-token", 1).delayMs).toBe( |
| 238 | 60_000, |
| 239 | ); |
| 240 | }); |
| 241 | |
| 242 | it("treats only authentication rejections as a dead credential", () => { |
| 243 | expect( |
| 244 | isRejectedBootstrapCredentialError( |
| 245 | new ConnectionBlockedError({ reason: "authentication", detail: "invalid" }), |
| 246 | ), |
| 247 | ).toBe(true); |
| 248 | expect( |
| 249 | isRejectedBootstrapCredentialError( |
| 250 | new ConnectionTransientError({ reason: "endpoint-unavailable", detail: "booting" }), |
| 251 | ), |
| 252 | ).toBe(false); |
| 253 | }); |
| 254 | }); |
| 255 | |
| 256 | describe("desktop-local bearer cache across token changes", () => { |
| 257 | it("keeps a live bearer when only the bootstrap token rotates", () => { |
| 258 | // Cached by endpoint: a rotated token reuses the bearer instead of |
| 259 | // dropping the environment (and its drafts) when the new token is rejected. |
| 260 | const cached = { |
| 261 | signature: "http://a|ws://a", |
| 262 | registration: {} as never, |
| 263 | expiresAtEpochMs: 20_000, |
| 264 | refreshAtEpochMs: 15_000, |
| 265 | }; |
| 266 | |
| 267 | expect(canReuseCachedPlatformRegistration(cached, "http://a|ws://a", 10_000)).toBe(true); |
| 268 | expect( |
| 269 | canRetainCachedPlatformRegistrationAfterRefreshFailure(cached, "http://a|ws://a", 16_000), |
| 270 | ).toBe(true); |
| 271 | expect(canReuseCachedPlatformRegistration(cached, "http://b|ws://b", 10_000)).toBe(false); |
| 272 | }); |
| 273 | }); |
| 274 | |
| 275 | describe("primary topology cache", () => { |
| 276 | const registration = {} as never; |
| 277 | const cached = { |
| 278 | signature: "primary|http://127.0.0.1:3773/|ws://127.0.0.1:3773/", |
| 279 | registration, |
| 280 | }; |
| 281 | const previous = new Map([[PRIMARY_LOCAL_ENVIRONMENT_ID, cached]]); |
| 282 | |
| 283 | it("captures synchronous primary target read failures", () => { |
| 284 | const cause = new Error("invalid primary target"); |
| 285 | |
| 286 | expect( |
| 287 | readPrimaryEnvironmentTargetResult(() => { |
| 288 | throw cause; |
| 289 | }), |
| 290 | ).toEqual({ _tag: "Failure", cause }); |
| 291 | }); |
| 292 | |
| 293 | it("retains the cached primary after a transient topology read failure", () => { |
| 294 | expect( |
| 295 | primaryRegistrationToRetainAfterTopologyRead(previous, { |
| 296 | _tag: "Failure", |
| 297 | cause: new Error("IPC unavailable"), |
| 298 | }), |
| 299 | ).toBe(cached); |
| 300 | }); |
| 301 | |
| 302 | it("treats a successful primary absence as authoritative removal", () => { |
| 303 | expect( |
| 304 | primaryRegistrationToRetainAfterTopologyRead(previous, { |
| 305 | _tag: "Success", |
| 306 | target: null, |
| 307 | }), |
| 308 | ).toBeUndefined(); |
| 309 | }); |
| 310 | }); |