Files
webstudio/apps/builder/app/services/github-issue-report.server.test.ts
Oleg Isonen 659e953869 fix: Preserve transitions and animation preview behavior (#6602)
Transition variables no longer get copied into unrelated timing fields.
Generated Text/Stagger animations use the saved `slidingWindow` prop,
while older `charWindow` configurations still work. Easing moves in the
expected direction. Windows preview-install reports include the process
failure code when available.

Closes #6598
Ref #6593
Ref #6599
Ref #6594

The animation runtime lives in [sdk-components-animation
#38](https://github.com/webstudio-is/sdk-components-animation/pull/38).
The private PR is merged; this PR points to its merged commit. #6593 was
tested on its exact saved project in headless Chromium and closed as not
reproducible; the confirmed prop leak is fixed here. #6594 was closed
after a documented local repro attempt; this PR improves the next
report.

## Work

- [x] Reproduce the transition bug; add regression coverage in parsing
and generated CSS.
- [x] Preserve unresolved `transition` shorthands for the browser to
resolve.
- [x] Keep old `charWindow` values working in Text and Stagger
animations, with a red/green regression test in the linked private PR.
- [x] Reproduce and fix the saved animation prop mismatch in generated
helpers.
- [x] Verify text/stagger easing in unit tests and Chromium/Firefox.
- [x] Add safe preview-install process details for future Windows
reports.
- [x] Update Builder property help and both animation docs pages.
- [x] Run focused animation tests, typecheck, lint, package build, and
animation fixture build after the compatibility fix.
- [x] Point the animation submodule to the merged #38 commit.
- [x] Verify required CI on the final commit (all workflows passed; one
unrelated E2E shard passed on rerun).
- [x] Find the production share token and build #6593’s exact project
with its reported 0.307.0 packages.
- [x] Compare intermediate scroll styles on #6593’s exact project.
Visible targets animate in both builds; the leaked attributes disappear
with this fix.

Known limit: the reported static-target symptom in #6593 and the Windows
environment in #6594 were not reproduced. The confirmed animation prop
leak and reporting gap are fixed, without claiming the unreproduced
symptoms are solved.
2026-10-25 19:12:25 +01:00

257 lines
9.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",
errorType: "AssetRevisionConflictError",
requestId: "abc123-LIS",
httpStatus: 504,
elapsedMs: 17_000,
issues: [
{
path: ["assets", "0", "type"],
code: "invalid_value",
constraint: 'one of "font"|"image"|"video"|"file"',
},
],
},
recentTargets: [
{ tool: "screenshot", entityIds: [{ field: "pageId", id: "page-456" }] },
{
tool: "inspect-instance",
entityIds: [{ field: "instanceId", id: "instance-456" }],
},
],
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("- Recent targeted tool: `screenshot`");
expect(body).toContain(" - pageId: `page-456`");
expect(body).toContain("- Recent targeted tool: `inspect-instance`");
expect(body).toContain(" - instanceId: `instance-456`");
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("- Error type: AssetRevisionConflictError");
expect(body).toContain("- Request ID: `abc123-LIS`");
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" }],
};
responseReport.runtime.recentFailure.previewInstall = {
processErrorCode: "ETIMEDOUT",
terminated: true,
};
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)");
expect(body).toContain("Preview install: ETIMEDOUT; process terminated");
responseReport.runtime.recentFailure.previewInstall = {};
expect(formatIssueReport(responseReport)).not.toContain("Preview install:");
});
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),
});
});
});