Files
webstudio/apps/builder/app/services/github-issue-report.server.test.ts
Oleg Isonen 60c549c46f fix: preserve local styles and improve issue diagnostics (#6408)
Fixes #6386

## Summary

- Keep local `ws:style` declarations after `ws:tokens` so instance
styles override token defaults.
- Capture privacy-safe HTTP timing/status, exceptional session namespace
states, preview freshness/version agreement, and validation paths in
issue reports.
- Derive reportable validation roots from the public operation catalog
instead of maintaining a duplicate allowlist.
- Keep captured failures current by clearing them after a successful
tool call.

## Verification

- Protocol issue-report tests: 3 passed
- Project build MCP: 128 tests passed
- CLI project session: 27 tests passed
- Builder issue reporter: 5 tests passed
- Relevant package typechecks passed
- Generated API metadata is clean
2026-09-17 20:13:27 +02:00

203 lines
6.8 KiB
TypeScript

import { describe, expect, test } from "vitest";
import type { IssueReportInput } from "@webstudio-is/protocol";
import {
createGitHubInstallationToken,
formatIssueReport,
publishIssueReport,
} from "./github-issue-report.server";
const report: IssueReportInput = {
trigger: "automatic-friction",
category: "schema-or-docs-mismatch",
deduplicationKey: "update-props-input-contract",
title: "fix: Clarify the update-props input contract",
agent: {
client: "Codex",
clientVersion: "1.2.3",
provider: "OpenAI",
model: "gpt-5.6-sol",
reasoningEffort: "medium",
},
runtime: {
cliVersion: "1.2.3",
nodeVersion: "22.14.0",
os: "linux",
osVersion: "6",
architecture: "arm64",
executionMode: "mcp",
apiContractVersion: "public-api:client",
bundleVersion: "bundle:client",
recentFailure: {
tool: "preview.start",
code: "PROJECT_BUNDLE_INVALID",
httpStatus: 504,
elapsedMs: 17_000,
issues: [
{
path: ["assets", "0", "type"],
code: "invalid_value",
constraint: 'one of "font"|"image"|"video"|"file"',
},
],
},
session: {
staleNamespaces: ["styles"],
missingNamespaces: ["instances"],
invalidatedNamespaces: [],
},
preview: {
stale: true,
hasRenderedVersion: true,
renderedVersionMatchesSession: false,
},
},
report: {
userStory:
"As a Webstudio user, I want routine MCP edits to complete without corrective retries.",
summary: "A documented operation required a corrected retry.",
attemptedWorkflow: ["Inspect the component.", "Attempt the update."],
expectedBehavior: "The documented input should be accepted.",
actualResult: "The initial call returned BAD_REQUEST.",
recoveryAttempts: ["Inspect the schema and retry."],
userImpact: "The edit required extra tool calls.",
technicalContext: "The update-props input shape was ambiguous.",
acceptanceCriteria: ["The documented input succeeds."],
},
};
describe("GitHub issue reports", () => {
test("formats the complete LLM-authored workflow and agent runtime", () => {
const body = formatIssueReport(report);
expect(body).toContain("## User story");
expect(body).toContain(report.report.userStory);
expect(body).toContain("## Expected behavior");
expect(body).toContain(report.report.actualResult);
expect(body).toContain("- Client: Codex 1.2.3");
expect(body).toContain("- CLI: 1.2.3");
expect(body).toContain("- Node.js: 22.14.0");
expect(body).toContain("- Operating system: linux 6 (arm64)");
expect(body).toContain("- Execution mode: mcp");
expect(body).toContain("- API contract: public-api:client");
expect(body).toContain("- Bundle contract: bundle:client");
expect(body).toContain("## Captured failure diagnostics");
expect(body).toContain("- Tool: `preview.start`");
expect(body).toContain("- Error code: `PROJECT_BUNDLE_INVALID`");
expect(body).toContain("- HTTP status: `504`");
expect(body).toContain("- Duration: `17000ms`");
expect(body).toContain("stale=styles");
expect(body).toContain("version matches session=false");
expect(body).toContain(
'- `assets.0.type`: `invalid_value` (one of "font"|"image"|"video"|"file")'
);
expect(body).toContain("- Model: gpt-5.6-sol");
expect(body).toContain("- Reasoning effort: medium");
expect(body).toContain(
"<!-- webstudio-issue-report:update-props-input-contract -->"
);
});
test("keeps version fields visible for reports from legacy clients", () => {
const legacyReport = structuredClone(report);
delete legacyReport.runtime;
const body = formatIssueReport(legacyReport);
expect(body).toContain("## Technical runtime");
expect(body).toContain("- CLI: unknown");
expect(body).toContain("- Node.js: unknown");
});
test("discovers the repository installation before creating its token", async () => {
const requests: Array<{ url: URL; init?: RequestInit }> = [];
const request: typeof fetch = async (input, init) => {
const url = new URL(input.toString());
requests.push({ url, init });
if (url.pathname === "/repos/webstudio-is/webstudio/installation") {
return Response.json({ id: 123 });
}
return Response.json({ token: "installation-token" });
};
await expect(
createGitHubInstallationToken({
appId: "app-id",
privateKey: "private-key",
createJwt: () => "app-jwt",
request,
})
).resolves.toBe("installation-token");
expect(requests.map(({ url }) => url.pathname)).toEqual([
"/repos/webstudio-is/webstudio/installation",
"/app/installations/123/access_tokens",
]);
expect(requests[0]?.init?.headers).toMatchObject({
Authorization: "Bearer app-jwt",
});
});
test("returns an existing issue with the same deduplication key", async () => {
const requests: URL[] = [];
const request: typeof fetch = async (input) => {
requests.push(new URL(input.toString()));
return Response.json({
items: [
{
number: 6020,
html_url: "https://github.com/webstudio-is/webstudio/issues/6020",
},
],
});
};
await expect(
publishIssueReport(report, {
getInstallationToken: async () => "installation-token",
request,
})
).resolves.toEqual({
status: "existing",
issueNumber: 6020,
issueUrl: "https://github.com/webstudio-is/webstudio/issues/6020",
});
expect(requests).toHaveLength(1);
expect(requests[0]?.searchParams.get("q")).toContain(
"webstudio-issue-report:update-props-input-contract"
);
});
test("creates a new issue using only the authored report", async () => {
const requests: Array<{ url: URL; init?: RequestInit }> = [];
const request: typeof fetch = async (input, init) => {
const url = new URL(input.toString());
requests.push({ url, init });
if (url.pathname === "/search/issues") {
return Response.json({ items: [] });
}
return Response.json({
number: 6021,
html_url: "https://github.com/webstudio-is/webstudio/issues/6021",
});
};
await expect(
publishIssueReport(report, {
getInstallationToken: async () => "installation-token",
request,
})
).resolves.toEqual({
status: "created",
issueNumber: 6021,
issueUrl: "https://github.com/webstudio-is/webstudio/issues/6021",
});
const createRequest = requests[1];
expect(createRequest?.url.pathname).toBe(
"/repos/webstudio-is/webstudio/issues"
);
expect(JSON.parse(String(createRequest?.init?.body))).toEqual({
title: report.title,
body: formatIssueReport(report),
});
});
});