FN-7890: cap report-a-bug GitHub URL by encoded length, not raw body length

Fixes the Report-a-Bug flow producing a GitHub "request URL too long" error by budgeting truncation against the actual encoded URL GitHub receives instead of the raw diagnostics body length.

- Replace the raw BUG_URL_BODY_CAP (5500 chars) with BUG_URL_MAX_ENCODED (8000 chars), measured against the final GitHub issue URL (base URL + ?body= + encodeURIComponent(body)).
- Add buildBugReportIssueUrl() which binary-searches the largest body prefix (by code point) whose encoded URL still fits the budget, appending a truncation marker when needed.
- doReportBug now calls buildBugReportIssueUrl(body) instead of manually slicing the body and encoding it inline.
- Update tests to assert the final URL length stays under BUG_URL_MAX_ENCODED and to exercise a diagnostics bundle whose JSON (quotes/braces) expands significantly under percent-encoding.

Files changed:
 .../__tests__/SystemControlsArea.test.tsx          |  9 +++--
 .../command-center/areas/SystemControlsArea.tsx    | 47 +++++++++++++++++-----
 2 files changed, 44 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-7890

Fusion-Task-Lineage: fcadafcd-0bbc-4c06-b2ec-c2b124d736db

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 18:10:55 -07:00
parent c341b58bde
commit dabedcf79c
2 changed files with 44 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import "@testing-library/jest-dom";
import { CommandCenter } from "../CommandCenter";
import { BUG_URL_MAX_ENCODED } from "../areas/SystemControlsArea";
const apiMock = vi.fn();
const mockFetchSystemInfo = vi.fn();
@@ -266,6 +267,8 @@ describe("SystemControlsArea layout integration", () => {
expect(body).toContain("<details><summary>Diagnostics</summary>");
expect(body).toContain('"recentLogs"');
expect(body).toContain("boom");
expect(body).not.toContain("\u2026(truncated)");
expect(url.length).toBeLessThanOrEqual(BUG_URL_MAX_ENCODED);
confirmSpy.mockRestore();
openSpy.mockRestore();
@@ -314,12 +317,12 @@ describe("SystemControlsArea layout integration", () => {
openSpy.mockRestore();
});
it("truncates an oversized diagnostics bundle in the bug report body with the truncation cap marker", async () => {
it("truncates an oversized diagnostics bundle against the encoded GitHub URL ceiling", 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)}`,
message: `error-line-${i}-{\"quoted\":\"${"x".repeat(100)}\",\"nested\":{\"value\":\"${"y".repeat(100)}\"}}`,
})),
});
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
@@ -332,8 +335,8 @@ describe("SystemControlsArea layout integration", () => {
await waitFor(() => expect(openSpy).toHaveBeenCalledTimes(1));
const url = openSpy.mock.calls[0]?.[0] as string;
const body = decodeURIComponent(url.split("?body=")[1] ?? "");
expect(url.length).toBeLessThanOrEqual(BUG_URL_MAX_ENCODED);
expect(body).toContain("\u2026(truncated)");
expect(body.length).toBeLessThanOrEqual(5500 + "\n\u2026(truncated)".length);
confirmSpy.mockRestore();
openSpy.mockRestore();

View File

@@ -58,9 +58,34 @@ const BACK_ONLINE_RELOAD_DELAY_MS = 3000;
// respawn, unsupervised restart that stopped) doesn't leave the panel polling
// forever with every control disabled.
const RESTART_WAIT_TIMEOUT_MS = 90_000;
const BUG_URL_BODY_CAP = 5500;
export const BUG_URL_MAX_ENCODED = 8000;
const BUG_URL_TRUNCATION_MARKER = "\n…(truncated)";
const BUG_URL_BODY_QUERY_PREFIX = "?body=";
const GITHUB_NEW_ISSUE_URL = "https://github.com/Runfusion/Fusion/issues/new";
function buildBugReportIssueUrl(body: string): string {
const prefix = `${GITHUB_NEW_ISSUE_URL}${BUG_URL_BODY_QUERY_PREFIX}`;
const toUrl = (candidate: string) => `${prefix}${encodeURIComponent(candidate)}`;
const fullUrl = toUrl(body);
if (fullUrl.length <= BUG_URL_MAX_ENCODED) return fullUrl;
const encodedBudget = BUG_URL_MAX_ENCODED - prefix.length;
const codePoints = Array.from(body);
let low = 0;
let high = codePoints.length;
while (low < high) {
const mid = Math.ceil((low + high) / 2);
const candidate = `${codePoints.slice(0, mid).join("")}${BUG_URL_TRUNCATION_MARKER}`;
if (encodeURIComponent(candidate).length <= encodedBudget) {
low = mid;
} else {
high = mid - 1;
}
}
return toUrl(`${codePoints.slice(0, low).join("")}${BUG_URL_TRUNCATION_MARKER}`);
}
type RestartPhase = null | "waiting" | "back" | "timeout";
interface SystemControlsAreaProps {
@@ -378,11 +403,16 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
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.
covering that whole bundle.
FNXC:SystemPanel 2026-07-12-18:41:
Requirement change (FN-7890): truncation is budgeted against the full URL that
GitHub receives: base issue URL + ?body= + encodeURIComponent(body). FN-7883's
diagnostics JSON includes quotes, braces, and newlines that expand during
percent-encoding, so the old raw body-length cap could still emit a request
URL too long for GitHub. The confirm gate, fenceSafe neutralization, and body
sections remain unchanged while the final window.open URL is kept under the
encoded ceiling.
*/
const doReportBug = useCallback(
() =>
@@ -396,7 +426,7 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
),
);
const diagnostics = includeDiagnostics ? await buildDiagnostics() : null;
let body = [
const body = [
"### What happened",
"",
"<!-- Describe the bug -->",
@@ -419,8 +449,7 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
]
: []),
].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");
window.open(buildBugReportIssueUrl(body), "_blank", "noopener");
}),
[buildDiagnostics, info, runAction, t],
);