Files
webstudio/apps/builder/app/services/github-issue-report.server.test.ts
T
Oleg Isonen 2570bcdd69 fix: preserve cloned bindings and Content template design (#6533)
Cloning a Collection can leave its item bindings attached to the
original Collection. Content-mode template insertion also drops fixed
image attributes, rich-text editing can remove inline images, and
YouTube previews retain the previous thumbnail after a URL edit.

Reuse the Builder fragment-copy path for cloning, preserve exact fixed
props from the same Content Block template, protect mixed image/text
content from destructive editing, and refresh video previews. Direct
component metadata lookup now includes hidden required parts without
adding them to component lists.

Unsupported Resource fields now return validation errors instead of
unchanged success. Destructive confirmation failures retain their reason
through the existing issue-report pipeline, and reports include the
negotiated server API contract alongside the CLI contract. Contract
versions still use the existing serialized-schema hash algorithm.

## Verification

- [x] Reproduce each behavior fix and demonstrate regression tests fail
before/pass after.
- [x] Verify native Content template insertion, content editing, and
reload through isolated local Builder E2E.
- [x] Check copy permissions: reject arbitrary design values,
reused/mismatched prop keys, changed clone structure, and copies from
another Content Block.
- [x] Remove the unused clone implementation and reuse the UI's
copy/remapping logic.
- [x] Review documentation impact; existing instructions remain
accurate, no documentation update needed.
- [x] Regenerate fixture builds from committed snapshots; generated
fixture files remain unchanged.
- [x] Complete generated API/docs checks, 2,492 project-build tests, 88
protocol tests, 99 focused CLI tests, package typechecks, lint, and
formatting.
- [x] Full Luna/low evaluation suite: 6/6 fixtures pass after preparing
the Content runtime build. Existing baselines are incompatible with this
model override; they were not updated.
- [x] Independent Astra review: fixed and re-tested Global Root cloning
and protected-prop overwrite findings; final review has no findings.
- [x] Required CI passes: all 19 main-workflow jobs, visual regression,
submodules, and PR lint.

Closes #6531
Closes #6519
Closes #6521
Ref #6523
Ref #6524
Ref #6525
Ref #6526
Ref #6527
Ref #6528
Ref #6529
2026-10-14 19:15:51 +02:00

234 lines
8.0 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: {
projectId: "project-123",
cliVersion: "1.2.3",
nodeVersion: "22.14.0",
os: "linux",
osVersion: "6",
architecture: "arm64",
executionMode: "mcp",
apiContractVersion: "public-api:client",
serverApiContractVersion: "public-api:server",
bundleVersion: "bundle:client",
recentFailure: {
entityIds: [
{ field: "pageId", id: "page-123" },
{ field: "instanceId", id: "instance-123" },
],
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("- Project ID: `project-123`");
expect(body).toContain("- pageId: `page-123`");
expect(body).toContain("- instanceId: `instance-123`");
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("- Server API contract: public-api:server");
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("formats response and browser diagnostics without raw data", () => {
const responseReport = structuredClone(report);
if (responseReport.runtime?.recentFailure === undefined) {
throw new Error("Expected failure diagnostics");
}
responseReport.runtime.recentFailure.response = {
format: "json",
envelope: "result",
batchSize: 1,
};
responseReport.runtime.recentFailure.browser = {
exitSignal: "SIGABRT",
attempts: [{ browser: "chromium", source: "path" }],
};
const body = formatIssueReport(responseReport);
expect(body).toContain("Response: json; envelope=result; batch size=1");
expect(body).toContain("Browser exit: SIGABRT");
expect(body).toContain("Browser attempts: chromium (path)");
});
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),
});
});
});