diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 79755cbc0bdc..96759d215031 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -62,8 +62,11 @@ describe("onboard (non-interactive): gateway and remote auth", () => { envSnapshot.restore(); }); - afterEach(() => { + afterEach(async () => { gatewayReachableState.mock = undefined; + const { resetSecretRedactionRegistryForTest } = + await import("../logging/secret-redaction-registry.test-support.js"); + resetSecretRedactionRegistryForTest(); testConfigStore.clear(); capturedReplaceConfigFileCalls.length = 0; configWritePluginLeaseDepths.length = 0; @@ -609,10 +612,17 @@ describe("onboard (non-interactive): gateway and remote auth", () => { it("emits structured JSON diagnostics when daemon health fails", async () => { await withStateDir("state-local-daemon-health-json-fail-", async (stateDir) => { + const registeredSecret = "qa-onboarding-health-secret"; + const { registerSecretValueForRedaction } = + await import("../logging/secret-redaction-registry.js"); + registerSecretValueForRedaction(registeredSecret); gatewayReachableState.mock = vi.fn(async () => ({ ok: false, - detail: "gateway closed (1006 abnormal closure (no close frame)): no close reason", + detail: `gateway closed (1006 abnormal closure (no close frame)): ${registeredSecret}`, })); + readLastGatewayErrorLineMock.mockResolvedValueOnce( + `Gateway failed to start: required secrets are unavailable: ${registeredSecret}`, + ); const { runtimeWithCapture, readCapturedJson } = createOnboardJsonCaptureRuntime(); await expectOnboardLocalJsonSetupFailure({ @@ -651,6 +661,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => { expect(parsed.diagnostics?.service?.runtimeStatus).toBe("running"); expect(parsed.diagnostics?.service?.pid).toBe(4242); expect(parsed.diagnostics?.lastGatewayError).toContain("required secrets are unavailable"); + expect(readCapturedJson()).not.toContain(registeredSecret); }); }, 60_000); @@ -694,12 +705,22 @@ describe("onboard (non-interactive): gateway and remote auth", () => { it("routes thrown health-check errors through the onboarding failure owner", async () => { await withStateDir("state-local-health-failure-text-", async (stateDir) => { + const registeredSecret = "qa-onboarding-health-secret"; + const { registerSecretValueForRedaction } = + await import("../logging/secret-redaction-registry.js"); + registerSecretValueForRedaction(registeredSecret); gatewayReachableState.mock = vi.fn(async () => ({ ok: true })); - healthCommandMock.mockRejectedValueOnce(new Error("health request timed out")); + healthCommandMock.mockRejectedValueOnce( + new Error(`health request timed out: ${registeredSecret}`), + ); - await expect( - runNonInteractiveSetup(createOnboardLocalDaemonOptions(stateDir), runtime), - ).rejects.toThrow(/health check failed[\s\S]*health request timed out/); + const failure = await runNonInteractiveSetup( + createOnboardLocalDaemonOptions(stateDir), + runtime, + ).catch((error: unknown) => error); + expect(failure).toBeInstanceOf(Error); + expect(String(failure)).toMatch(/health check failed[\s\S]*health request timed out/); + expect(String(failure)).not.toContain(registeredSecret); }); }, 60_000); diff --git a/src/commands/onboard-non-interactive/local/output.ts b/src/commands/onboard-non-interactive/local/output.ts index 8f7ef7d0ea40..8eba6dbb87e0 100644 --- a/src/commands/onboard-non-interactive/local/output.ts +++ b/src/commands/onboard-non-interactive/local/output.ts @@ -5,6 +5,7 @@ * are kept here so local and remote setup report failures consistently. */ import type { GatewayServiceLoadState } from "../../../daemon/service-types.js"; +import { redactSecrets } from "../../../logging/redact.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../../runtime.js"; import type { OnboardOptions } from "../../onboard-types.js"; @@ -197,8 +198,17 @@ export function logNonInteractiveOnboardingFailure(params: { }); const recoveryHint = recoveryHintForGatewayHealthFailure(classification); const hints = [...(recoveryHint ? [recoveryHint] : []), ...(params.hints?.filter(Boolean) ?? [])]; - const gatewayRuntime = formatGatewayRuntimeSummary(params.diagnostics); - const service = params.diagnostics?.service; + const output = redactSecrets({ + message: params.message, + detail: params.detail, + hints, + gateway: params.gateway, + daemonInstall: params.daemonInstall, + daemonRuntime: params.daemonRuntime, + diagnostics: params.diagnostics, + }); + const gatewayRuntime = formatGatewayRuntimeSummary(output.diagnostics); + const service = output.diagnostics?.service; const serviceLoadText = service ? service.loadState.status === "loaded" ? service.loadedText @@ -210,32 +220,32 @@ export function logNonInteractiveOnboardingFailure(params: { ok: false, mode: params.mode, phase: params.phase, - message: params.message, + message: output.message, classification, - detail: params.detail, - gateway: params.gateway, + detail: output.detail, + gateway: output.gateway, installDaemon: Boolean(params.installDaemon), - daemonInstall: params.daemonInstall, - daemonRuntime: params.daemonRuntime, - diagnostics: params.diagnostics, - hints: hints.length > 0 ? hints : undefined, + daemonInstall: output.daemonInstall, + daemonRuntime: output.daemonRuntime, + diagnostics: output.diagnostics, + hints: output.hints.length > 0 ? output.hints : undefined, }); return; } const lines = [ - params.message, + output.message, classification ? `Classification: ${classification}` : undefined, - params.detail ? `Last probe: ${params.detail}` : undefined, + output.detail ? `Last probe: ${output.detail}` : undefined, service ? `Service: ${service.label} (${serviceLoadText})` : undefined, gatewayRuntime ? `Runtime: ${gatewayRuntime}` : undefined, - params.diagnostics?.lastGatewayError - ? `Last gateway error: ${params.diagnostics.lastGatewayError}` + output.diagnostics?.lastGatewayError + ? `Last gateway error: ${output.diagnostics.lastGatewayError}` : undefined, - params.diagnostics?.inspectError - ? `Diagnostics warning: ${params.diagnostics.inspectError}` + output.diagnostics?.inspectError + ? `Diagnostics warning: ${output.diagnostics.inspectError}` : undefined, - hints.length > 0 ? hints.join("\n") : undefined, + output.hints.length > 0 ? output.hints.join("\n") : undefined, ] .filter(Boolean) .join("\n");