mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
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
234 lines
8.0 KiB
TypeScript
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),
|
|
});
|
|
});
|
|
});
|