feat(cli): forward feedback submissions to the backend feedback endpoint (#2003)

* feat(cli): forward feedback submissions to backend endpoint

* fix(cli): truncate feedback fields to backend caps + ack before forwarding

Addresses PR review (via):
- Truncate comment (2000) / cli_version (100) / env (500) to the backend DTO
  caps before POSTing, so a pasted stack trace is forwarded truncated instead
  of rejected with a 422 the best-effort path swallows silently.
- Print "Thanks for the feedback!" before the best-effort forward so the ack
  isn't blocked behind the (bounded) network call.

* fix(cli): type feedback fetch mock
This commit is contained in:
Miguel Ángel
2026-07-06 20:44:42 -04:00
committed by GitHub
parent 1005703441
commit 9fe06f30da
3 changed files with 132 additions and 0 deletions
@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const getPublishApiBaseUrlMock = vi.hoisted(() => vi.fn(() => "https://api.example.com"));
vi.mock("./publishProject.js", () => ({
getPublishApiBaseUrl: getPublishApiBaseUrlMock,
}));
import { submitFeedback } from "./submitFeedback.js";
describe("submitFeedback", () => {
beforeEach(() => {
getPublishApiBaseUrlMock.mockReturnValue("https://api.example.com");
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it("posts feedback to the backend endpoint", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitFeedback({
rating: 4,
comment: "fast but font missing",
cliVersion: "1.2.3",
env: "os=darwin/arm64 node=v22.11.0",
});
expect(fetchMock).toHaveBeenCalledOnce();
expect(getPublishApiBaseUrlMock).toHaveBeenCalledOnce();
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/v1/hyperframes/feedback",
expect.objectContaining({
method: "POST",
headers: { "content-type": "application/json", heygen_route: "canary" },
body: JSON.stringify({
rating: 4,
comment: "fast but font missing",
cli_version: "1.2.3",
env: "os=darwin/arm64 node=v22.11.0",
}),
signal: expect.any(AbortSignal),
}),
);
});
it("truncates over-long fields to the backend caps", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitFeedback({
rating: 3,
comment: "x".repeat(2500),
cliVersion: "v".repeat(200),
env: "e".repeat(600),
});
const requestInit = fetchMock.mock.calls[0]?.[1];
expect(requestInit).toBeDefined();
const body = JSON.parse(requestInit?.body as string);
expect(body.comment).toHaveLength(2000);
expect(body.cli_version).toHaveLength(100);
expect(body.env).toHaveLength(500);
});
it("does not reject when fetch rejects", async () => {
const fetchMock = vi.fn().mockRejectedValueOnce(new TypeError("fetch failed"));
vi.stubGlobal("fetch", fetchMock);
await expect(
submitFeedback({ rating: 1, cliVersion: "1.2.3", env: "os=linux" }),
).resolves.toBeUndefined();
});
it("always resolves regardless of fetch outcome", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(null, { status: 500 }))
.mockRejectedValueOnce(new Error("offline"));
vi.stubGlobal("fetch", fetchMock);
await expect(submitFeedback({ rating: 2, cliVersion: "1.2.3" })).resolves.toBeUndefined();
await expect(submitFeedback({ rating: 3, cliVersion: "1.2.3" })).resolves.toBeUndefined();
});
});