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:
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user