mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 00:27:43 +09:00
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
203 lines
6.8 KiB
TypeScript
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),
|
|
});
|
|
});
|
|
});
|