mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix: preserve early failure diagnostics
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
readDiagnosticStabilityBundleFileSync,
|
||||
readLatestDiagnosticStabilityBundleSync,
|
||||
resetDiagnosticStabilityBundleForTest,
|
||||
writeDiagnosticStabilityBundleForFailureSync,
|
||||
writeDiagnosticStabilityBundleSync,
|
||||
type DiagnosticStabilityBundle,
|
||||
} from "./diagnostic-stability-bundle.js";
|
||||
@@ -110,6 +111,33 @@ describe("diagnostic stability bundles", () => {
|
||||
expect(fs.existsSync(path.join(tempDir, "logs", "stability"))).toBe(false);
|
||||
});
|
||||
|
||||
it("writes failure bundles even when the recorder snapshot is empty", () => {
|
||||
const result = writeDiagnosticStabilityBundleForFailureSync(
|
||||
"gateway.restart_startup_failed",
|
||||
Object.assign(new Error("raw startup config payload"), { code: "ERR_CONFIG_PARSE" }),
|
||||
{
|
||||
stateDir: tempDir,
|
||||
now: new Date("2026-04-22T12:00:00.000Z"),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("written");
|
||||
const bundle = readBundle(result.status === "written" ? result.path : "");
|
||||
const raw = fs.readFileSync(result.status === "written" ? result.path : "", "utf8");
|
||||
expect(bundle).toMatchObject({
|
||||
reason: "gateway.restart_startup_failed",
|
||||
error: {
|
||||
name: "Error",
|
||||
code: "ERR_CONFIG_PARSE",
|
||||
},
|
||||
snapshot: {
|
||||
count: 0,
|
||||
events: [],
|
||||
},
|
||||
});
|
||||
expect(raw).not.toContain("raw startup config payload");
|
||||
});
|
||||
|
||||
it("registers a fatal hook only while installed", () => {
|
||||
startDiagnosticStabilityRecorder();
|
||||
emitDiagnosticEvent({ type: "webhook.received", channel: "telegram" });
|
||||
|
||||
@@ -365,7 +365,7 @@ export function writeDiagnosticStabilityBundleForFailureSync(
|
||||
...options,
|
||||
reason,
|
||||
error,
|
||||
includeEmpty: false,
|
||||
includeEmpty: true,
|
||||
});
|
||||
if (result.status === "written") {
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import JSZip from "jszip";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { emitDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js";
|
||||
import {
|
||||
resetDiagnosticStabilityBundleForTest,
|
||||
@@ -549,4 +549,51 @@ describe("diagnostic support export", () => {
|
||||
expect(combined).toContain("log-tail-read-failed");
|
||||
expect(combined).toContain("sanitized log tail unavailable");
|
||||
});
|
||||
|
||||
it("keeps writing when config stat fails", async () => {
|
||||
const fakeToken = "sk-test-config-stat-secret-token-1234567890";
|
||||
const configPath = path.join(tempDir, "openclaw.json");
|
||||
const outputPath = path.join(tempDir, "support-failed-config-stat.zip");
|
||||
fs.writeFileSync(configPath, "{}\n", "utf8");
|
||||
|
||||
const originalStatSync = fs.statSync.bind(fs);
|
||||
const statSpy = vi.spyOn(fs, "statSync").mockImplementation((target, options) => {
|
||||
if (target === configPath) {
|
||||
throw new Error(`config stat failed with token ${fakeToken}`);
|
||||
}
|
||||
return originalStatSync(target, options as never);
|
||||
});
|
||||
|
||||
try {
|
||||
await writeDiagnosticSupportExport({
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_STATE_DIR: tempDir,
|
||||
},
|
||||
stateDir: tempDir,
|
||||
outputPath,
|
||||
now: new Date("2026-04-22T12:00:03.000Z"),
|
||||
readLogTail: async () => ({
|
||||
file: path.join(tempDir, "logs", "openclaw.log"),
|
||||
cursor: 0,
|
||||
size: 0,
|
||||
truncated: false,
|
||||
reset: false,
|
||||
lines: [],
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
statSpy.mockRestore();
|
||||
}
|
||||
|
||||
const entries = await readZipTextEntries(outputPath);
|
||||
const combined = Object.values(entries).join("\n");
|
||||
expect(Object.keys(entries).toSorted()).toContain("config/shape.json");
|
||||
expect(combined).not.toContain(fakeToken);
|
||||
expect(combined).toContain('"parseOk": false');
|
||||
expect(combined).toContain("config stat failed with token");
|
||||
expect(combined).toContain("Attach this zip to the bug report");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -284,19 +284,22 @@ function configShapeReadFailure(params: {
|
||||
return shape;
|
||||
}
|
||||
|
||||
function isMissingPathError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object" || !("code" in error)) {
|
||||
return false;
|
||||
}
|
||||
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
||||
}
|
||||
|
||||
function readConfigExport(options: {
|
||||
configPath: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
stateDir: string;
|
||||
}): ConfigExport {
|
||||
const redactedConfigPath = redactPathForSupport(options.configPath, options);
|
||||
const stat = fs.existsSync(options.configPath) ? fs.statSync(options.configPath) : null;
|
||||
if (!stat) {
|
||||
return {
|
||||
shape: configShapeReadFailure({ configPath: redactedConfigPath, redaction: options }),
|
||||
};
|
||||
}
|
||||
let stat: fs.Stats | undefined;
|
||||
try {
|
||||
stat = fs.statSync(options.configPath);
|
||||
const parsed = parseConfigJson5(fs.readFileSync(options.configPath, "utf8"));
|
||||
if (!parsed.ok) {
|
||||
return {
|
||||
@@ -313,12 +316,13 @@ function readConfigExport(options: {
|
||||
sanitized: sanitizeConfigDetails(parsed.parsed, options),
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
shape: configShapeReadFailure({
|
||||
configPath: redactedConfigPath,
|
||||
redaction: options,
|
||||
stat,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: !stat && isMissingPathError(error) ? undefined : errorMessage,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user