From 1be8ffce3bdb772ed6c715e86262fcf2e1793564 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sun, 20 Sep 2026 09:35:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=AF=BC=E6=BC=94=E5=8F=B0?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=20-=20=E9=9A=94=E7=A6=BB=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=A4=8D=E7=8E=B0=E5=85=A5=E5=8F=A3=E7=9A=84=E5=A4=96=E8=A7=82?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/progress/pending-test.mdx | 6 ++ web/src/components/layout/app-providers.tsx | 5 +- web/src/lib/dev-repro.ts | 3 + web/src/main.tsx | 7 +- web/test/appearance-bootstrap.test.ts | 2 + web/test/application-bootstrap.test.mjs | 76 +++++++++++++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 web/src/lib/dev-repro.ts create mode 100644 web/test/application-bootstrap.test.mjs diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index fcc8eef12..718a6c1f2 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -69,6 +69,12 @@ PR #543 本次未合入:其已发布迁移校验和改写、管理员显式图 - [ ] 检查浅色、深色与窄屏下的费用区域、模型表格横向滚动及长金额换行。 - [ ] 新建或编辑模型,在“模型展示名(一级目录)”选择其他渠道已有分组;输入新分组保存后再次编辑,确认下拉更新且创作端归组正确。 +## CI 检查修复与镜像构建提速 + +- [x] 修复导演台 DEV 复现入口在 React 初始化前请求 `/api/public/appearance` 导致的 E2E 502:入口和 Provider 共用精确路径隔离,普通页面与生产环境仍执行外观初始化,未放宽浏览器错误断言。 +- [x] 外观启动与导演台专项测试 54 项通过,包含执行真实 `main.tsx` 分支的 9 项新增回归;生产构建(含 TypeScript 检查)通过,仍有 chunk 大小告警。 +- [ ] 在 GitHub 重跑 Director P0 Chrome E2E,确认六个场景不再出现外观接口 502;本轮未启动本地开发服务器或执行 Chrome E2E,不以入口回归测试替代浏览器验收。 + ## 画布素材链路与工作条改造(2026-09-19) 已实现: diff --git a/web/src/components/layout/app-providers.tsx b/web/src/components/layout/app-providers.tsx index 827a0c551..c35fef7aa 100644 --- a/web/src/components/layout/app-providers.tsx +++ b/web/src/components/layout/app-providers.tsx @@ -9,6 +9,7 @@ import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader" import { getAntThemeConfig } from "@/lib/app-theme"; import { applySkinTheme } from "@/lib/skin-themes"; import { appQueryClient } from "@/lib/query-client"; +import { isIsolatedDirectorRepro } from "@/lib/dev-repro"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import { applyAppearanceMetadata, useAppearanceStore } from "@/stores/use-appearance-store"; import { useUserStore } from "@/stores/use-user-store"; @@ -35,8 +36,8 @@ export function AppProviders({ children }: { children: ReactNode }) { // DEV 复现台必须是同源本地确定性场景:AuthSessionHydrator 会打 /api/auth/session, // ClientRootInit 会打 /api/model-catalog,没有后端时产生真实 502,与导演台无关却会污染判据。 - // 只精确匹配该路径;生产构建中 import.meta.env.DEV 为 false,本分支被摇树删除。 - const isolateDevRepro = import.meta.env.DEV && typeof window !== "undefined" && window.location.pathname === "/dev/director-repro"; + // 与启动入口共用精确路径边界;生产构建中 import.meta.env.DEV 为 false,始终不启用隔离。 + const isolateDevRepro = typeof window !== "undefined" && isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname); return ( diff --git a/web/src/lib/dev-repro.ts b/web/src/lib/dev-repro.ts new file mode 100644 index 000000000..3dad14044 --- /dev/null +++ b/web/src/lib/dev-repro.ts @@ -0,0 +1,3 @@ +export function isIsolatedDirectorRepro(dev: boolean, pathname: string): boolean { + return dev && pathname === "/dev/director-repro"; +} diff --git a/web/src/main.tsx b/web/src/main.tsx index 221db2319..b3e3dbd13 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,7 +1,12 @@ import "@fontsource-variable/inter"; import "@fontsource-variable/jetbrains-mono"; import { bootstrapAppearance } from "@/services/appearance-bootstrap"; +import { isIsolatedDirectorRepro } from "@/lib/dev-repro"; // The public film entry checks its availability independently of workspace bootstrap. if (/^\/welcome\/?$/.test(window.location.pathname)) void import("./welcome-application"); -else void bootstrapAppearance().finally(() => import("./application")); +else { + // The backend-free DEV lab must not make requests before AppProviders isolates it. + const appearanceReady = isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname) ? Promise.resolve() : bootstrapAppearance(); + void appearanceReady.finally(() => import("./application")); +} diff --git a/web/test/appearance-bootstrap.test.ts b/web/test/appearance-bootstrap.test.ts index 6dc31cc52..7ace0c2ba 100644 --- a/web/test/appearance-bootstrap.test.ts +++ b/web/test/appearance-bootstrap.test.ts @@ -7,6 +7,8 @@ test("initial HTML stays brand neutral until the public appearance is resolved", expect(html).not.toContain("影策"); expect(html).toContain("正在加载"); + expect(mainSource).toContain("bootstrapAppearance()"); + expect(mainSource).toContain('import("./application")'); expect(mainSource.indexOf("bootstrapAppearance()")).toBeLessThan(mainSource.indexOf('import("./application")')); }); diff --git a/web/test/application-bootstrap.test.mjs b/web/test/application-bootstrap.test.mjs new file mode 100644 index 000000000..a123092c3 --- /dev/null +++ b/web/test/application-bootstrap.test.mjs @@ -0,0 +1,76 @@ +import { expect, test } from "bun:test"; +import { runInNewContext } from "node:vm"; + +import { isIsolatedDirectorRepro } from "../src/lib/dev-repro"; + +// Execute the real entry point; replace only its font, network and UI side effects. +async function prepareEntry(dev, pathname) { + const build = await Bun.build({ + entrypoints: [new URL("../src/main.tsx", import.meta.url).pathname], + target: "browser", + format: "iife", + define: { "import.meta.env.DEV": JSON.stringify(dev) }, + plugins: [ + { + name: "entry-side-effects", + setup(builder) { + builder.onResolve({ filter: /^(@fontsource-variable\/|@\/services\/appearance-bootstrap$|\.\/(welcome-)?application$)/ }, ({ path }) => ({ path, namespace: "entry-test" })); + builder.onLoad({ filter: /.*/, namespace: "entry-test" }, ({ path }) => { + if (path.startsWith("@fontsource-variable/")) return { contents: "", loader: "js" }; + if (path === "@/services/appearance-bootstrap") { + return { contents: 'export function bootstrapAppearance() { events.push("appearance"); return appearanceReady; }', loader: "js" }; + } + return { contents: `events.push(${JSON.stringify(path)}); entryLoaded();`, loader: "js" }; + }); + }, + }, + ], + }); + expect(build.success).toBe(true); + + const events = []; + let resolveAppearance; + let entryLoaded; + const appearanceReady = new Promise((resolve) => (resolveAppearance = resolve)); + const loaded = new Promise((resolve) => (entryLoaded = resolve)); + runInNewContext(await build.outputs[0].text(), { window: { location: { pathname } }, events, appearanceReady, entryLoaded }); + return { events, resolveAppearance, loaded }; +} + +test("DEV director lab loads without calling the appearance backend", async () => { + const entry = await prepareEntry(true, "/dev/director-repro"); + await entry.loaded; + expect(entry.events).toEqual(["./application"]); +}); + +for (const [dev, pathname] of [ + [false, "/dev/director-repro"], + [true, "/login"], + [false, "/login"], + [true, "/dev/director-repro/"], + [true, "/dev/director-repro-other"], +]) { + test(`appearance still blocks normal startup: dev=${dev} path=${pathname}`, async () => { + const entry = await prepareEntry(dev, pathname); + expect(entry.events).toEqual(["appearance"]); + entry.resolveAppearance(); + await entry.loaded; + expect(entry.events).toEqual(["appearance", "./application"]); + }); +} + +for (const pathname of ["/welcome", "/welcome/"]) { + test(`public film entry remains independent: ${pathname}`, async () => { + const entry = await prepareEntry(false, pathname); + await entry.loaded; + expect(entry.events).toEqual(["./welcome-application"]); + }); +} + +test("provider isolation shares the exact DEV-only route boundary", () => { + expect(isIsolatedDirectorRepro(true, "/dev/director-repro")).toBe(true); + expect(isIsolatedDirectorRepro(false, "/dev/director-repro")).toBe(false); + for (const path of ["/", "/login", "/dev/director-repro/", "/dev/director-repro-other"]) { + expect(isIsolatedDirectorRepro(true, path)).toBe(false); + } +});