FN-7883: expand Command Center bug report with full diagnostics bundle

Replace the bug-report flow's last-5-error-lines excerpt with the full buildDiagnostics() bundle (health, runtime/system info, recent logs) behind a single confirmation prompt.
- doReportBug now reuses buildDiagnostics() instead of independently fetching a 5-line error excerpt
- Single confirm() prompt covers the whole diagnostics bundle instead of only recent errors
- Diagnostics are embedded as a collapsible <details> JSON code block under a new ### Diagnostics section
- Preserved existing safeguards: fenceSafe() neutralization of embedded code fences and BUG_URL_BODY_CAP truncation
- Added tests covering: full bundle inclusion on confirm, omission on decline, fence-breakout neutralization, and oversized-bundle truncation

Files changed:
 .../__tests__/SystemControlsArea.test.tsx          | 96 ++++++++++++++++++++++
 .../command-center/areas/SystemControlsArea.tsx    | 53 +++++++-----
 2 files changed, 128 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-7883

Fusion-Task-Lineage: 44dd26de-d0f6-427b-a082-59d580f31674

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 17:42:39 -07:00
parent 2e7fce21ae
commit 3a37f48e78
2 changed files with 128 additions and 21 deletions

View File

@@ -243,6 +243,102 @@ describe("SystemControlsArea layout integration", () => {
expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
});
it("embeds the full diagnostics bundle in the bug report body when the operator confirms", async () => {
mockFetchSystemLogs.mockResolvedValue({
entries: [{ timestamp: "2026-07-12T00:00:00.000Z", level: "error", message: "boom" }],
});
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
await renderSystemTab();
const reportCard = await screen.findByTestId("cc-syscontrol-report-bug");
fireEvent.click(within(reportCard).getByRole("button", { name: "Report" }));
await waitFor(() => expect(openSpy).toHaveBeenCalledTimes(1));
expect(confirmSpy).toHaveBeenCalledTimes(1);
const url = openSpy.mock.calls[0]?.[0] as string;
const body = decodeURIComponent(url.split("?body=")[1] ?? "");
expect(url).toContain("github.com");
expect(url).toContain("/issues/new");
expect(body).toContain("### What happened");
expect(body).toContain("### Environment");
expect(body).toContain("### Diagnostics");
expect(body).toContain("<details><summary>Diagnostics</summary>");
expect(body).toContain('"recentLogs"');
expect(body).toContain("boom");
confirmSpy.mockRestore();
openSpy.mockRestore();
});
it("omits the diagnostics block from the bug report body when the operator declines", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
await renderSystemTab();
const reportCard = await screen.findByTestId("cc-syscontrol-report-bug");
fireEvent.click(within(reportCard).getByRole("button", { name: "Report" }));
await waitFor(() => expect(openSpy).toHaveBeenCalledTimes(1));
const url = openSpy.mock.calls[0]?.[0] as string;
const body = decodeURIComponent(url.split("?body=")[1] ?? "");
expect(body).toContain("### What happened");
expect(body).toContain("### Environment");
expect(body).not.toContain("### Diagnostics");
confirmSpy.mockRestore();
openSpy.mockRestore();
});
it("neutralizes embedded code fences in log messages so they cannot break out of the diagnostics fence", async () => {
mockFetchSystemLogs.mockResolvedValue({
entries: [{ timestamp: "2026-07-12T00:00:00.000Z", level: "error", message: "```oops``` breakout" }],
});
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
await renderSystemTab();
const reportCard = await screen.findByTestId("cc-syscontrol-report-bug");
fireEvent.click(within(reportCard).getByRole("button", { name: "Report" }));
await waitFor(() => expect(openSpy).toHaveBeenCalledTimes(1));
const url = openSpy.mock.calls[0]?.[0] as string;
const body = decodeURIComponent(url.split("?body=")[1] ?? "");
// The fenced JSON block should contain no raw ``` other than the fence delimiters themselves.
const jsonBlockMatch = body.match(/```json\n([\s\S]*?)\n```/);
expect(jsonBlockMatch).not.toBeNull();
expect(jsonBlockMatch?.[1]).not.toContain("```");
expect(jsonBlockMatch?.[1]).toContain("'''oops''' breakout");
confirmSpy.mockRestore();
openSpy.mockRestore();
});
it("truncates an oversized diagnostics bundle in the bug report body with the truncation cap marker", async () => {
mockFetchSystemLogs.mockResolvedValue({
entries: Array.from({ length: 100 }, (_, i) => ({
timestamp: "2026-07-12T00:00:00.000Z",
level: "error" as const,
message: `error-line-${i}-${"x".repeat(100)}`,
})),
});
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
await renderSystemTab();
const reportCard = await screen.findByTestId("cc-syscontrol-report-bug");
fireEvent.click(within(reportCard).getByRole("button", { name: "Report" }));
await waitFor(() => expect(openSpy).toHaveBeenCalledTimes(1));
const url = openSpy.mock.calls[0]?.[0] as string;
const body = decodeURIComponent(url.split("?body=")[1] ?? "");
expect(body).toContain("\u2026(truncated)");
expect(body.length).toBeLessThanOrEqual(5500 + "\n\u2026(truncated)".length);
confirmSpy.mockRestore();
openSpy.mockRestore();
});
it("keeps the System controls header row override active on mobile", () => {
const css = readFileSync(join(process.cwd(), "app/components/command-center/areas/SystemControlsArea.css"), "utf8");

View File

@@ -371,29 +371,31 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
[buildDiagnostics, runAction, t, toast],
);
/*
FNXC:SystemPanel 2026-07-12-15:30:
Requirement change (FN-7883): the bug-report flow previously offered only the
last 5 error log lines behind confirmation. That gave maintainers too little
context for a first triage pass. Now doReportBug reuses the exact same
buildDiagnostics() bundle that "Copy diagnostics" produces (health,
runtime/system info, recent logs) and asks a single confirmation question
covering that whole bundle. The confirm gate, fenceSafe neutralization, and
BUG_URL_BODY_CAP truncation are preserved unchanged because this content is
still sent to a public github.com issue and must never be included without
explicit operator consent, must not let a log line break out of the fenced
code block, and must not produce an over-length URL.
*/
const doReportBug = useCallback(
() =>
runAction("report-bug", async () => {
const health = await fetchDashboardHealth().catch(() => null);
const recentErrors = info?.logsSupported
? await fetchSystemLogs(200)
.then((r) => r.entries.filter((entry) => entry.level === "error").slice(-5))
.catch(() => [])
: [];
// FNXC:SystemPanel 2026-07-12-14:05: The recent-errors excerpt is
// server log content sent to github.com. Require explicit confirmation
// before including it (operator may not want internal logs public), and
// neutralize embedded ``` so a log line can't break out of the fence.
const includeErrors =
recentErrors.length > 0 &&
window.confirm(
t(
"systemControls.reportBugConfirm",
"Include the last {{count}} server error log line(s) in the GitHub issue? They will be sent to github.com — review after the issue opens.",
{ count: recentErrors.length },
),
);
const fenceSafe = (text: string) => text.replace(/`/g, "'");
const includeDiagnostics = window.confirm(
t(
"systemControls.reportBugConfirm",
"Include diagnostic info and recent logs (health, runtime info, recent server logs) in the GitHub issue? They will be sent to github.com — review after the issue opens.",
),
);
const diagnostics = includeDiagnostics ? await buildDiagnostics() : null;
let body = [
"### What happened",
"",
@@ -404,14 +406,23 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
`- Platform: ${info?.platform ?? "unknown"} (${info?.arch ?? "?"}), Node ${info?.nodeVersion ?? "?"}`,
`- Uptime: ${info?.uptimeSeconds ?? "?"}s, supervised: ${info?.supervised ?? false}`,
"",
...(includeErrors
? ["### Recent server errors", "```", ...recentErrors.map((entry) => fenceSafe(`${entry.prefix ? `[${entry.prefix}] ` : ""}${entry.message}`)), "```"]
...(diagnostics
? [
"### Diagnostics",
"<details><summary>Diagnostics</summary>",
"",
"```json",
fenceSafe(JSON.stringify(diagnostics, null, 2)),
"```",
"",
"</details>",
]
: []),
].join("\n");
if (body.length > BUG_URL_BODY_CAP) body = `${body.slice(0, BUG_URL_BODY_CAP)}\n…(truncated)`;
window.open(`${GITHUB_NEW_ISSUE_URL}?body=${encodeURIComponent(body)}`, "_blank", "noopener");
}),
[info, runAction],
[buildDiagnostics, info, runAction, t],
);
// ── Control definitions ───────────────────────────────────────────────────