fix(onboard): redact gateway health failures (#127071)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-21 00:43:16 -07:00
committed by GitHub
parent 8ac7dd254c
commit 76af07b735
2 changed files with 53 additions and 22 deletions
@@ -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);
@@ -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");