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 @@...@@ -1,9 +1,14 @@
1import * as RelayClient from "@t3tools/shared/relayClient";1import * as RelayClient from "@t3tools/shared/relayClient";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";
4import * as Console from "effect/Console";
3import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Logger from "effect/Logger";
4import * as Option from "effect/Option";8import * as Option from "effect/Option";
9import * as References from "effect/References";
510
6import { acquireRelayClientForLink } from "./connect.ts";11import { acquireRelayClientForLink, reportCloudDisconnectResults } from "./connect.ts";
712
8const managedExecutable = {13const managedExecutable = {
9 status: "available",14 status: "available",
...@@ -100,3 +105,51 @@ it.effect("reuses an available relay client executable without prompting", () =>...@@ -100,3 +105,51 @@ it.effect("reuses an available relay client executable without prompting", () =>
100 assert.equal(promptCalls, 0);105 assert.equal(promptCalls, 0);
101 }),106 }),
102);107);
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 {...@@ -7,6 +7,7 @@ import {
7import { RelayOkResponse } from "@t3tools/contracts/relay";7import { RelayOkResponse } from "@t3tools/contracts/relay";
8import * as RelayClient from "@t3tools/shared/relayClient";8import * as RelayClient from "@t3tools/shared/relayClient";
9import { withRelayClientTracing } from "@t3tools/shared/relayTracing";9import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
10import * as Cause from "effect/Cause";
10import * as Console from "effect/Console";11import * as Console from "effect/Console";
11import * as Duration from "effect/Duration";12import * as Duration from "effect/Duration";
12import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
...@@ -179,7 +180,7 @@ const withCloudCliSessionToken = <A, E, R>(...@@ -179,7 +180,7 @@ const withCloudCliSessionToken = <A, E, R>(
179type LiveCloudActionResult =180type LiveCloudActionResult =
180 | { readonly status: "not-running" }181 | { readonly status: "not-running" }
181 | { readonly status: "succeeded" }182 | { readonly status: "succeeded" }
182 | { readonly status: "failed"; readonly cause: unknown };183 | { readonly status: "failed"; readonly cause: Cause.Cause<unknown> };
183184
184const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {
185 const config = yield* ServerConfig.ServerConfig;186 const config = yield* ServerConfig.ServerConfig;
...@@ -211,6 +212,21 @@ type RelayUnlinkResult =...@@ -211,6 +212,21 @@ type RelayUnlinkResult =
211 | { readonly status: "revoked" }212 | { readonly status: "revoked" }
212 | { readonly status: "not-linked" };213 | { 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
214const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(function* () {230const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(function* () {
215 const tokens = yield* CliTokenManager.CloudCliTokenManager;231 const tokens = yield* CliTokenManager.CloudCliTokenManager;
216 const token = yield* tokens.getExisting;232 const token = yield* tokens.getExisting;
...@@ -236,6 +252,42 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f...@@ -236,6 +252,42 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f
236 : ({ status: "not-linked" } satisfies RelayUnlinkResult);252 : ({ status: "not-linked" } satisfies RelayUnlinkResult);
237});253});
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
239const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {291const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
240 readonly clearAuthorization: boolean;292 readonly clearAuthorization: boolean;
241}) {293}) {
...@@ -249,23 +301,11 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {...@@ -249,23 +301,11 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
249 yield* tokens.clear;301 yield* tokens.clear;
250 }302 }
251303
252 if (liveResult.status === "failed") {304 yield* reportCloudDisconnectResults({
253 yield* Console.warn(305 clearAuthorization: options.clearAuthorization,
254 `T3 Connect is disabled, but the running server could not stop its tunnel: ${String(liveResult.cause)}\nRestart that server to stop the connector.`,306 liveResult,
255 );307 relayResult,
256 } else {308 });
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 }
269309
270 if (options.clearAuthorization) {310 if (options.clearAuthorization) {
271 yield* Console.log("Signed out of T3 Connect locally.");311 yield* Console.log("Signed out of T3 Connect locally.");