From 7184d068e8f20df537f32ca1b3f30ead2e6668f6 Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Fri, 30 Oct 2026 16:51:02 -0400 Subject: [PATCH 1/2] fix(server): t3 auth --json and --token-only print only the result Resolving the CLI's config sweeps abandoned attachment uploads and logs "Removed expired attachment uploads." at Info. Logs go to stdout, and `quietLogs` only quieted the layers built after config resolution, so the log line landed in front of `t3 auth session issue --token-only`, every `t3 auth ... --json`, `t3 environment list --json` and `t3 connect status --json`, breaking `$(...)` capture and JSON parsing. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cli/auth.ts | 2 +- apps/server/src/cli/config.ts | 12 ++++++++-- apps/server/src/cli/connect.ts | 2 +- apps/server/src/cli/environment.ts | 2 +- apps/server/src/cli/pair.test.ts | 38 ++++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/server/src/cli/auth.ts b/apps/server/src/cli/auth.ts index 06b949e3013ab..b75fba88bddc3 100644 --- a/apps/server/src/cli/auth.ts +++ b/apps/server/src/cli/auth.ts @@ -32,7 +32,7 @@ const runWithEnvironmentAuth = Effect.fn("auth.runWithEnvironmentAuth")(function }, ) { const logLevel = yield* GlobalFlag.LogLevel; - const config = yield* resolveCliAuthConfig(flags, logLevel); + const config = yield* resolveCliAuthConfig(flags, logLevel, options); const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel; return yield* Effect.gen(function* () { const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth; diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 00621e332deb9..720569b6137d8 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -15,6 +15,7 @@ import * as LogLevel from "effect/LogLevel"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Redacted from "effect/Redacted"; +import * as References from "effect/References"; import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; import * as SchemaTransformation from "effect/SchemaTransformation"; @@ -513,8 +514,9 @@ export const resolveServerConfig = Effect.fn("config.resolveServerConfig")(funct export const resolveCliAuthConfig = ( flags: CliAuthLocationFlags, cliLogLevel: Option.Option, -) => - resolveServerConfig( + options?: { readonly quietLogs?: boolean }, +) => { + const resolved = resolveServerConfig( { mode: Option.none(), port: Option.none(), @@ -531,6 +533,12 @@ export const resolveCliAuthConfig = ( }, cliLogLevel, ); + // Logs go to stdout, where --json and --token-only print, and resolving can + // log, for example when it sweeps abandoned attachment uploads. + return options?.quietLogs === true + ? resolved.pipe(Effect.provideService(References.MinimumLogLevel, "Error")) + : resolved; +}; const DurationShorthandPattern = /^(?\d+)(?ms|s|m|h|d|w)$/i; diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 134fa17f026eb..5e39625754ade 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -529,7 +529,7 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* { }), ); }); + +describe("scripted auth output", () => { + // Live clock: the sweep compares the upload's real mtime with the current time. + it.live.each( + [ + ["auth", "session", "issue", "--token-only"], + ["auth", "session", "list", "--json"], + ["auth", "pairing", "list", "--json"], + ].map((command) => ({ command, label: command.join(" ") })), + )("prints only the result for $label after removing an abandoned upload", ({ command }) => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-cli-quiet-test-")); + const attachmentsDir = NodePath.join(baseDir, "userdata", "attachments"); + NodeFS.mkdirSync(attachmentsDir, { recursive: true }); + // An abandoned upload makes config resolution log that it removed it. + const abandoned = NodePath.join(attachmentsDir, "abandoned.part"); + NodeFS.writeFileSync(abandoned, ""); + const jan2020Seconds = 1_577_836_800; + NodeFS.utimesSync(abandoned, jan2020Seconds, jan2020Seconds); + + const lines = yield* provideCliTestLayers( + Effect.gen(function* () { + yield* runCli([...command, "--base-dir", baseDir]); + return yield* TestConsole.logLines; + }), + ); + + assert.isFalse(NodeFS.existsSync(abandoned)); + assert.lengthOf(lines, 1); + const output = String(lines[0]); + if (command.includes("--json")) { + assert.deepEqual(JSON.parse(output), []); + } else { + assert.match(output.trim(), /^[\w-]+\.[\w-]+/); + } + }), + ); +}); From 62998c9ebfc5263ffb147aaf941181e610ed230f Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Fri, 30 Oct 2026 17:05:32 -0400 Subject: [PATCH 2/2] test(server): age the abandoned upload with TestClock Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cli/pair.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 659f0a2bd2adb..88717f9a7f549 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -18,6 +18,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { FetchHttpClient } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import * as TestClock from "effect/testing/TestClock"; import * as TestConsole from "effect/testing/TestConsole"; import { Command, CliError } from "effect/cli"; @@ -550,8 +551,7 @@ describe("auth pairing create --base-url", () => { }); describe("scripted auth output", () => { - // Live clock: the sweep compares the upload's real mtime with the current time. - it.live.each( + it.effect.each( [ ["auth", "session", "issue", "--token-only"], ["auth", "session", "list", "--json"], @@ -567,6 +567,8 @@ describe("scripted auth output", () => { NodeFS.writeFileSync(abandoned, ""); const jan2020Seconds = 1_577_836_800; NodeFS.utimesSync(abandoned, jan2020Seconds, jan2020Seconds); + // The sweep reads Clock, so a week past the upload's mtime makes it expired. + yield* TestClock.setTime((jan2020Seconds + 7 * 24 * 60 * 60) * 1000); const lines = yield* provideCliTestLayers( Effect.gen(function* () {