authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:12:22-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:12:22-07:00
logb51aef10c4f21c35f195b81e548c052041a15051
treefef78d8c888b2ed0b3ce8b869b529606857c5665
parentfc2cdeceb9ddb28c0de834b9fcb38abf68d99148
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve cloud disconnect diagnostics (#3437)

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

2 files changed, 112 insertions(+), 19 deletions(-)

apps/server/src/cli/connect.test.ts+54-1
......@@ -1,9 +1,14 @@
11import * as RelayClient from "@t3tools/shared/relayClient";
22import { assert, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";
4import * as Console from "effect/Console";
35import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Logger from "effect/Logger";
48import * as Option from "effect/Option";
9import * as References from "effect/References";
510
6import { acquireRelayClientForLink } from "./connect.ts";
11import { acquireRelayClientForLink, reportCloudDisconnectResults } from "./connect.ts";
712
813const managedExecutable = {
914 status: "available",
......@@ -100,3 +105,51 @@ it.effect("reuses an available relay client executable without prompting", () =>
100105 assert.equal(promptCalls, 0);
101106 }),
102107);
108
109it.effect("keeps disconnect causes in structured logs and out of console warnings", () => {
110 const warnings: ReadonlyArray<unknown>[] = [];
111 const logs: Readonly<Record<string, unknown>>[] = [];
112 const testConsole = {
113 ...globalThis.console,
114 warn: (...args: ReadonlyArray<unknown>) => {
115 warnings.push(args);
116 },
117 } satisfies Console.Console;
118 const logger = Logger.make(({ fiber }) => {
119 logs.push(fiber.getRef(References.CurrentLogAnnotations));
120 });
121 const liveFailure = "live unlink private diagnostic";
122 const relayFailure = "relay revoke private diagnostic";
123
124 return reportCloudDisconnectResults({
125 clearAuthorization: true,
126 liveResult: {
127 status: "failed",
128 cause: Cause.fail(new Error(liveFailure)),
129 },
130 relayResult: Exit.failCause(Cause.die(new Error(relayFailure))),
131 }).pipe(
132 Effect.provideService(Console.Console, testConsole),
133 Effect.provide(Logger.layer([logger], { mergeWithExisting: false })),
134 Effect.tap(() =>
135 Effect.sync(() => {
136 assert.lengthOf(warnings, 2);
137 const warningText = warnings.flat().map(String).join("\n");
138 assert.include(warningText, "running server could not stop its tunnel");
139 assert.include(warningText, "Could not revoke the relay-side environment record");
140 assert.notInclude(warningText, liveFailure);
141 assert.notInclude(warningText, relayFailure);
142 assert.deepEqual(
143 logs.map(({ operation, clearAuthorization }) => ({ operation, clearAuthorization })),
144 [
145 { operation: "live-server-unlink", clearAuthorization: true },
146 { operation: "relay-environment-unlink", clearAuthorization: true },
147 ],
148 );
149 const loggedCauses = logs.map((log) => String(log.cause)).join("\n");
150 assert.include(loggedCauses, liveFailure);
151 assert.include(loggedCauses, relayFailure);
152 }),
153 ),
154 );
155});
apps/server/src/cli/connect.ts+58-18
......@@ -7,6 +7,7 @@ import {
77import { RelayOkResponse } from "@t3tools/contracts/relay";
88import * as RelayClient from "@t3tools/shared/relayClient";
99import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
10import * as Cause from "effect/Cause";
1011import * as Console from "effect/Console";
1112import * as Duration from "effect/Duration";
1213import * as Effect from "effect/Effect";
......@@ -179,7 +180,7 @@ const withCloudCliSessionToken = <A, E, R>(
179180type LiveCloudActionResult =
180181 | { readonly status: "not-running" }
181182 | { readonly status: "succeeded" }
182 | { readonly status: "failed"; readonly cause: unknown };
183 | { readonly status: "failed"; readonly cause: Cause.Cause<unknown> };
183184
184185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {
185186 const config = yield* ServerConfig.ServerConfig;
......@@ -211,6 +212,21 @@ type RelayUnlinkResult =
211212 | { readonly status: "revoked" }
212213 | { readonly status: "not-linked" };
213214
215type CloudDisconnectOperation = "live-server-unlink" | "relay-environment-unlink";
216
217const logCloudDisconnectFailure = (
218 operation: CloudDisconnectOperation,
219 clearAuthorization: boolean,
220 cause: Cause.Cause<unknown>,
221) =>
222 Effect.logWarning("T3 Connect disconnect operation failed.").pipe(
223 Effect.annotateLogs({
224 operation,
225 clearAuthorization,
226 cause: Cause.pretty(cause),
227 }),
228 );
229
214230const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(function* () {
215231 const tokens = yield* CliTokenManager.CloudCliTokenManager;
216232 const token = yield* tokens.getExisting;
......@@ -236,6 +252,42 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f
236252 : ({ status: "not-linked" } satisfies RelayUnlinkResult);
237253});
238254
255export const reportCloudDisconnectResults = Effect.fn("cloud.cli.report_disconnect_results")(
256 function* (input: {
257 readonly clearAuthorization: boolean;
258 readonly liveResult: LiveCloudActionResult;
259 readonly relayResult: Exit.Exit<RelayUnlinkResult, unknown>;
260 }) {
261 if (input.liveResult.status === "failed") {
262 yield* logCloudDisconnectFailure(
263 "live-server-unlink",
264 input.clearAuthorization,
265 input.liveResult.cause,
266 );
267 yield* Console.warn(
268 "T3 Connect is disabled, but the running server could not stop its tunnel.\nRestart that server to stop the connector.",
269 );
270 } else {
271 yield* Console.log("T3 Connect is disabled locally.");
272 }
273
274 if (Exit.isFailure(input.relayResult)) {
275 yield* logCloudDisconnectFailure(
276 "relay-environment-unlink",
277 input.clearAuthorization,
278 input.relayResult.cause,
279 );
280 yield* Console.warn(
281 input.clearAuthorization
282 ? "Could not revoke the relay-side environment record before signing out.\nThe stored CLI authorization was still removed locally."
283 : "Could not revoke the relay-side environment record yet.\nRun `t3 connect unlink` again when the relay is reachable.",
284 );
285 } else if (input.relayResult.value.status === "revoked") {
286 yield* Console.log("Revoked the relay-side environment record.");
287 }
288 },
289);
290
239291const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
240292 readonly clearAuthorization: boolean;
241293}) {
......@@ -249,23 +301,11 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
249301 yield* tokens.clear;
250302 }
251303
252 if (liveResult.status === "failed") {
253 yield* Console.warn(
254 `T3 Connect is disabled, but the running server could not stop its tunnel: ${String(liveResult.cause)}\nRestart that server to stop the connector.`,
255 );
256 } else {
257 yield* Console.log("T3 Connect is disabled locally.");
258 }
259
260 if (Exit.isFailure(relayResult)) {
261 yield* Console.warn(
262 options.clearAuthorization
263 ? `Could not revoke the relay-side environment record before signing out: ${String(relayResult.cause)}\nThe stored CLI authorization was still removed locally.`
264 : `Could not revoke the relay-side environment record yet: ${String(relayResult.cause)}\nRun \`t3 connect unlink\` again when the relay is reachable.`,
265 );
266 } else if (relayResult.value.status === "revoked") {
267 yield* Console.log("Revoked the relay-side environment record.");
268 }
304 yield* reportCloudDisconnectResults({
305 clearAuthorization: options.clearAuthorization,
306 liveResult,
307 relayResult,
308 });
269309
270310 if (options.clearAuthorization) {
271311 yield* Console.log("Signed out of T3 Connect locally.");