diff --git a/apps/server/src/cli/connect.test.ts b/apps/server/src/cli/connect.test.ts index 5fce3bc1cd7..70b0329ac90 100644 --- a/apps/server/src/cli/connect.test.ts +++ b/apps/server/src/cli/connect.test.ts @@ -1,9 +1,14 @@ import * as RelayClient from "@t3tools/shared/relayClient"; import { assert, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; +import * as References from "effect/References"; -import { acquireRelayClientForLink } from "./connect.ts"; +import { acquireRelayClientForLink, reportCloudDisconnectResults } from "./connect.ts"; const managedExecutable = { status: "available", @@ -100,3 +105,51 @@ it.effect("reuses an available relay client executable without prompting", () => assert.equal(promptCalls, 0); }), ); + +it.effect("keeps disconnect causes in structured logs and out of console warnings", () => { + const warnings: ReadonlyArray[] = []; + const logs: Readonly>[] = []; + const testConsole = { + ...globalThis.console, + warn: (...args: ReadonlyArray) => { + warnings.push(args); + }, + } satisfies Console.Console; + const logger = Logger.make(({ fiber }) => { + logs.push(fiber.getRef(References.CurrentLogAnnotations)); + }); + const liveFailure = "live unlink private diagnostic"; + const relayFailure = "relay revoke private diagnostic"; + + return reportCloudDisconnectResults({ + clearAuthorization: true, + liveResult: { + status: "failed", + cause: Cause.fail(new Error(liveFailure)), + }, + relayResult: Exit.failCause(Cause.die(new Error(relayFailure))), + }).pipe( + Effect.provideService(Console.Console, testConsole), + Effect.provide(Logger.layer([logger], { mergeWithExisting: false })), + Effect.tap(() => + Effect.sync(() => { + assert.lengthOf(warnings, 2); + const warningText = warnings.flat().map(String).join("\n"); + assert.include(warningText, "running server could not stop its tunnel"); + assert.include(warningText, "Could not revoke the relay-side environment record"); + assert.notInclude(warningText, liveFailure); + assert.notInclude(warningText, relayFailure); + assert.deepEqual( + logs.map(({ operation, clearAuthorization }) => ({ operation, clearAuthorization })), + [ + { operation: "live-server-unlink", clearAuthorization: true }, + { operation: "relay-environment-unlink", clearAuthorization: true }, + ], + ); + const loggedCauses = logs.map((log) => String(log.cause)).join("\n"); + assert.include(loggedCauses, liveFailure); + assert.include(loggedCauses, relayFailure); + }), + ), + ); +}); diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 314680b0d80..3ce53391fa6 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -7,6 +7,7 @@ import { import { RelayOkResponse } from "@t3tools/contracts/relay"; import * as RelayClient from "@t3tools/shared/relayClient"; import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; +import * as Cause from "effect/Cause"; import * as Console from "effect/Console"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -179,7 +180,7 @@ const withCloudCliSessionToken = ( type LiveCloudActionResult = | { readonly status: "not-running" } | { readonly status: "succeeded" } - | { readonly status: "failed"; readonly cause: unknown }; + | { readonly status: "failed"; readonly cause: Cause.Cause }; const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () { const config = yield* ServerConfig.ServerConfig; @@ -211,6 +212,21 @@ type RelayUnlinkResult = | { readonly status: "revoked" } | { readonly status: "not-linked" }; +type CloudDisconnectOperation = "live-server-unlink" | "relay-environment-unlink"; + +const logCloudDisconnectFailure = ( + operation: CloudDisconnectOperation, + clearAuthorization: boolean, + cause: Cause.Cause, +) => + Effect.logWarning("T3 Connect disconnect operation failed.").pipe( + Effect.annotateLogs({ + operation, + clearAuthorization, + cause: Cause.pretty(cause), + }), + ); + const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(function* () { const tokens = yield* CliTokenManager.CloudCliTokenManager; const token = yield* tokens.getExisting; @@ -236,6 +252,42 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f : ({ status: "not-linked" } satisfies RelayUnlinkResult); }); +export const reportCloudDisconnectResults = Effect.fn("cloud.cli.report_disconnect_results")( + function* (input: { + readonly clearAuthorization: boolean; + readonly liveResult: LiveCloudActionResult; + readonly relayResult: Exit.Exit; + }) { + if (input.liveResult.status === "failed") { + yield* logCloudDisconnectFailure( + "live-server-unlink", + input.clearAuthorization, + input.liveResult.cause, + ); + yield* Console.warn( + "T3 Connect is disabled, but the running server could not stop its tunnel.\nRestart that server to stop the connector.", + ); + } else { + yield* Console.log("T3 Connect is disabled locally."); + } + + if (Exit.isFailure(input.relayResult)) { + yield* logCloudDisconnectFailure( + "relay-environment-unlink", + input.clearAuthorization, + input.relayResult.cause, + ); + yield* Console.warn( + input.clearAuthorization + ? "Could not revoke the relay-side environment record before signing out.\nThe stored CLI authorization was still removed locally." + : "Could not revoke the relay-side environment record yet.\nRun `t3 connect unlink` again when the relay is reachable.", + ); + } else if (input.relayResult.value.status === "revoked") { + yield* Console.log("Revoked the relay-side environment record."); + } + }, +); + const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: { readonly clearAuthorization: boolean; }) { @@ -249,23 +301,11 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: { yield* tokens.clear; } - if (liveResult.status === "failed") { - yield* Console.warn( - `T3 Connect is disabled, but the running server could not stop its tunnel: ${String(liveResult.cause)}\nRestart that server to stop the connector.`, - ); - } else { - yield* Console.log("T3 Connect is disabled locally."); - } - - if (Exit.isFailure(relayResult)) { - yield* Console.warn( - options.clearAuthorization - ? `Could not revoke the relay-side environment record before signing out: ${String(relayResult.cause)}\nThe stored CLI authorization was still removed locally.` - : `Could not revoke the relay-side environment record yet: ${String(relayResult.cause)}\nRun \`t3 connect unlink\` again when the relay is reachable.`, - ); - } else if (relayResult.value.status === "revoked") { - yield* Console.log("Revoked the relay-side environment record."); - } + yield* reportCloudDisconnectResults({ + clearAuthorization: options.clearAuthorization, + liveResult, + relayResult, + }); if (options.clearAuthorization) { yield* Console.log("Signed out of T3 Connect locally.");