mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
## Summary This improves how agents discover, author, inspect, and verify Webstudio projects while reducing unnecessary MCP context usage. ## What changed - Adds an **Agents** section to Project Settings with ready-to-run setup commands and client configuration for Claude Code, Codex, Cursor, and VS Code. - Makes large list and schema responses compact and paginated by default, with **verbose** available when complete records or diagnostics are needed. - Applies consistent compact output to assets, instances, styles, design tokens, CLI schemas, and project-session metadata. - Returns actionable expression warnings for props, resources, and Collection bindings, including unknown variables and risky render-time mutation resources. - Improves Collection expression guidance so agents can bind array items with the correct scoped variables. - Reports rendered audit outcomes explicitly as complete, partial, confirmation-required, or failed. - Expands runtime output schemas so MCP clients can validate responses reliably. - Strengthens the packaged CLI smoke workflow with a realistic seven-page stdio MCP scenario. ## Architecture - Consolidates `project-build` from 65 file-oriented entrypoints to 11 architecture-owned surfaces. - Separates the core model, contracts, runtime authoring, transfer, state, project sessions, MCP, visual tooling, persistence codecs, and authenticated server services. - Isolates Node-only JSX evaluation from browser-safe transfer utilities and authenticated server modules. - Keeps persistence codecs independent from authenticated database services, preventing unrelated module initialization. - Defines shared instance-path types once and reuses them across runtime modules. - Preserves Immer initialization through explicit package side-effect metadata. - Adds package tests that prevent a return to source-file entrypoints or generic exposure of Node-only surfaces. ## Validation - Full repository checks, tests, typechecks, generated-file checks, and fixtures - All 163 Builder test files and 2,141 tests - All 71 project-build test files and 1,794 tests - Lint and package-boundary validation - Full packed CLI release smoke test, including install, preview, agent setup, seven-page stdio MCP workflow, rendered audit, and structured commands
164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { createDefaultPages } from "@webstudio-is/project-build";
|
|
import type { CompactBuild } from "@webstudio-is/project-build";
|
|
import { createEmptyWebstudioFragment } from "@webstudio-is/project-build/runtime";
|
|
import type { Asset } from "@webstudio-is/sdk";
|
|
import {
|
|
createBuilderRuntimeState,
|
|
executeApiRuntimeOperation,
|
|
} from "./api-runtime.server";
|
|
|
|
const createBuild = (): CompactBuild =>
|
|
({
|
|
id: "build-1",
|
|
projectId: "project-1",
|
|
version: 1,
|
|
pages: createDefaultPages({ rootInstanceId: "root-1" }),
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "root-1",
|
|
component: "Body",
|
|
children: [],
|
|
},
|
|
],
|
|
props: [],
|
|
styles: [],
|
|
styleSources: [],
|
|
styleSourceSelections: [],
|
|
dataSources: [],
|
|
resources: [],
|
|
assets: [],
|
|
breakpoints: [],
|
|
}) as unknown as CompactBuild;
|
|
|
|
describe("api runtime adapter", () => {
|
|
test("normalizes compact build arrays into builder state maps", () => {
|
|
const asset = { id: "asset-1", name: "image.png" } as Asset;
|
|
const state = createBuilderRuntimeState(createBuild(), [asset]);
|
|
|
|
expect(state.pages?.homePageId).toBeDefined();
|
|
expect(state.instances?.get("root-1")).toMatchObject({
|
|
id: "root-1",
|
|
component: "Body",
|
|
});
|
|
expect(state.assets?.get("asset-1")).toEqual(asset);
|
|
expect(state.assets?.get("asset-1")).not.toBe(asset);
|
|
});
|
|
|
|
test("executes runtime reads and maps runtime errors to public api errors", async () => {
|
|
const build = createBuild();
|
|
|
|
await expect(
|
|
executeApiRuntimeOperation({
|
|
id: "pages.list",
|
|
build,
|
|
input: { projectId: "project-1" },
|
|
})
|
|
).resolves.toMatchObject({
|
|
pages: [expect.objectContaining({ isHome: true })],
|
|
});
|
|
|
|
await expect(
|
|
executeApiRuntimeOperation({
|
|
id: "pages.get",
|
|
build,
|
|
input: { pageId: "missing" },
|
|
})
|
|
).rejects.toThrow("Page not found");
|
|
});
|
|
|
|
test("strips API transport fields before strict runtime validation", async () => {
|
|
await expect(
|
|
executeApiRuntimeOperation({
|
|
id: "project.search",
|
|
build: createBuild(),
|
|
input: { projectId: "project-1", query: "Home" },
|
|
})
|
|
).resolves.toMatchObject({ query: "Home" });
|
|
});
|
|
|
|
test("preserves actionable validation issues in public api errors", async () => {
|
|
await expect(
|
|
executeApiRuntimeOperation({
|
|
id: "pages.create",
|
|
build: createBuild(),
|
|
input: { projectId: "project-1", name: false, path: "/pricing" },
|
|
})
|
|
).rejects.toMatchObject({
|
|
code: "BAD_REQUEST",
|
|
cause: expect.objectContaining({
|
|
webstudioCode: "INVALID_INPUT",
|
|
issues: [
|
|
expect.objectContaining({
|
|
path: ["name"],
|
|
constraint: "type:string",
|
|
}),
|
|
],
|
|
}),
|
|
} satisfies Partial<TRPCError>);
|
|
});
|
|
|
|
test("awaits async runtime mutations before reading mutation payload", async () => {
|
|
const mutation = await executeApiRuntimeOperation({
|
|
id: "instances.insertComponent",
|
|
build: createBuild(),
|
|
input: {
|
|
projectId: "project-1",
|
|
parentInstanceId: "root-1",
|
|
component: "Form",
|
|
},
|
|
});
|
|
|
|
expect(mutation).toMatchObject({
|
|
payload: expect.arrayContaining([
|
|
expect.objectContaining({ namespace: "instances" }),
|
|
]),
|
|
result: {
|
|
rootInstanceIds: expect.arrayContaining([expect.any(String)]),
|
|
instanceIds: expect.arrayContaining([expect.any(String)]),
|
|
},
|
|
});
|
|
});
|
|
|
|
test("executes semantic Collection insertion through the server runtime", async () => {
|
|
const mutation = await executeApiRuntimeOperation({
|
|
id: "instances.insertCollection",
|
|
build: createBuild(),
|
|
input: {
|
|
projectId: "project-1",
|
|
parentInstanceId: "root-1",
|
|
data: { type: "json", value: [{ title: "First" }] },
|
|
itemFragment: {
|
|
...createEmptyWebstudioFragment(),
|
|
children: [{ type: "id", value: "item-root" }],
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "item-root",
|
|
component: "ws:element",
|
|
tag: "article",
|
|
children: [],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(mutation).toMatchObject({
|
|
payload: expect.arrayContaining([
|
|
expect.objectContaining({ namespace: "instances" }),
|
|
expect.objectContaining({ namespace: "props" }),
|
|
expect.objectContaining({ namespace: "dataSources" }),
|
|
]),
|
|
result: {
|
|
collectionInstanceId: expect.any(String),
|
|
itemRootInstanceId: expect.any(String),
|
|
itemParameterId: expect.any(String),
|
|
itemKeyParameterId: expect.any(String),
|
|
},
|
|
});
|
|
});
|
|
});
|