From 26bb5634bd133ca549b5d1caccd7311dff5e8a6b Mon Sep 17 00:00:00 2001 From: cen617-code <1057290604@qq.com> Date: Tue, 29 Sep 2026 09:55:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20release=20v1.0.5=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E7=B2=BE=E7=AE=80=E4=B8=8E=E7=BD=91=E7=AB=99=E5=8F=91?= =?UTF-8?q?=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 147 + docs/website-deployment.md | 2 +- package-lock.json | 4 +- package.json | 2 +- web_platform/e2e/builtinRobots.spec.ts | 3 + .../src/app/App.characterization.test.tsx | 232 ++ web_platform/src/app/App.loading.test.tsx | 1004 +++++ web_platform/src/app/App.tsx | 3547 ++++------------- .../src/app/components/WorkbenchDialogs.tsx | 59 + .../src/app/components/WorkbenchTools.tsx | 51 + .../app/components/WorkspaceNotices.test.tsx | 54 + .../src/app/components/WorkspaceNotices.tsx | 38 + web_platform/src/app/editorSelection.ts | 6 - web_platform/src/app/hooks/useAgentScene.ts | 110 + web_platform/src/app/hooks/useMapAuthoring.ts | 356 ++ web_platform/src/app/hooks/useMapEditor.ts | 365 ++ web_platform/src/app/hooks/useMapScene.ts | 491 +++ .../app/hooks/useMapWorkspaceState.test.ts | 99 + .../src/app/hooks/useMapWorkspaceState.ts | 151 + .../src/app/hooks/useModelLoading.test.ts | 151 + web_platform/src/app/hooks/useModelLoading.ts | 439 ++ .../src/app/hooks/usePolicyWorkflow.ts | 273 ++ .../src/app/hooks/useProjectImport.ts | 364 ++ .../src/app/hooks/useProjectSource.test.ts | 102 + .../src/app/hooks/useProjectSource.ts | 85 + .../src/app/hooks/usePythonController.ts | 128 + .../src/app/hooks/useSimulationActions.ts | 164 + .../src/app/hooks/useViewerLifecycle.test.tsx | 66 + .../src/app/hooks/useViewerLifecycle.ts | 203 + .../src/app/hooks/useWorkbenchChrome.test.tsx | 45 + .../src/app/hooks/useWorkbenchChrome.ts | 113 + .../app/hooks/useWorkbenchFeedback.test.ts | 65 + .../src/app/hooks/useWorkbenchFeedback.ts | 34 + .../app/hooks/useWorkbenchShortcuts.test.tsx | 60 + .../src/app/hooks/useWorkbenchShortcuts.ts | 45 + .../src/app/hooks/useWorkspaceDrop.test.ts | 124 + .../src/app/hooks/useWorkspaceDrop.ts | 128 + web_platform/src/app/websiteConfig.test.tsx | 99 + .../src/app/workbenchCommands.test.ts | 124 + web_platform/src/app/workbenchCommands.tsx | 202 + .../src/controller/PythonControllerRuntime.ts | 2 +- web_platform/src/project/cachedFiles.ts | 2 +- .../src/project/workspaceManifest.test.ts | 88 + web_platform/src/project/workspaceManifest.ts | 59 + web_platform/src/simulation/PhysicsAdapter.ts | 2 +- .../SimulationSession.snapshot.test.ts | 228 ++ .../src/simulation/SimulationSession.ts | 263 +- .../src/simulation/simulationTypes.ts | 96 + .../src/simulation/snapshotReaders.ts | 168 + ...calTrainingPanel.characterization.test.tsx | 96 + .../src/training/LocalTrainingPanel.tsx | 609 +-- web_platform/src/training/TrainingFields.tsx | 49 + .../src/training/TrainingJobSection.test.tsx | 68 + .../src/training/TrainingJobSection.tsx | 136 + .../src/training/TrainingTaskFields.tsx | 281 ++ .../src/training/trainingForm.test.ts | 168 + web_platform/src/training/trainingForm.ts | 116 + .../src/training/trainingPresentation.ts | 37 + web_platform/src/tuning/ScalarRingBuffer.ts | 12 +- web_platform/src/tuning/domain.ts | 7 - .../src/viewer/MuJoCoViewer.mesh.test.ts | 220 + web_platform/src/viewer/MuJoCoViewer.ts | 138 +- web_platform/src/viewer/geometryFactory.ts | 84 + web_platform/src/viewer/meshPresentation.ts | 72 + 64 files changed, 9048 insertions(+), 3688 deletions(-) create mode 100644 web_platform/src/app/App.characterization.test.tsx create mode 100644 web_platform/src/app/App.loading.test.tsx create mode 100644 web_platform/src/app/components/WorkbenchDialogs.tsx create mode 100644 web_platform/src/app/components/WorkbenchTools.tsx create mode 100644 web_platform/src/app/components/WorkspaceNotices.test.tsx create mode 100644 web_platform/src/app/components/WorkspaceNotices.tsx create mode 100644 web_platform/src/app/hooks/useAgentScene.ts create mode 100644 web_platform/src/app/hooks/useMapAuthoring.ts create mode 100644 web_platform/src/app/hooks/useMapEditor.ts create mode 100644 web_platform/src/app/hooks/useMapScene.ts create mode 100644 web_platform/src/app/hooks/useMapWorkspaceState.test.ts create mode 100644 web_platform/src/app/hooks/useMapWorkspaceState.ts create mode 100644 web_platform/src/app/hooks/useModelLoading.test.ts create mode 100644 web_platform/src/app/hooks/useModelLoading.ts create mode 100644 web_platform/src/app/hooks/usePolicyWorkflow.ts create mode 100644 web_platform/src/app/hooks/useProjectImport.ts create mode 100644 web_platform/src/app/hooks/useProjectSource.test.ts create mode 100644 web_platform/src/app/hooks/useProjectSource.ts create mode 100644 web_platform/src/app/hooks/usePythonController.ts create mode 100644 web_platform/src/app/hooks/useSimulationActions.ts create mode 100644 web_platform/src/app/hooks/useViewerLifecycle.test.tsx create mode 100644 web_platform/src/app/hooks/useViewerLifecycle.ts create mode 100644 web_platform/src/app/hooks/useWorkbenchChrome.test.tsx create mode 100644 web_platform/src/app/hooks/useWorkbenchChrome.ts create mode 100644 web_platform/src/app/hooks/useWorkbenchFeedback.test.ts create mode 100644 web_platform/src/app/hooks/useWorkbenchFeedback.ts create mode 100644 web_platform/src/app/hooks/useWorkbenchShortcuts.test.tsx create mode 100644 web_platform/src/app/hooks/useWorkbenchShortcuts.ts create mode 100644 web_platform/src/app/hooks/useWorkspaceDrop.test.ts create mode 100644 web_platform/src/app/hooks/useWorkspaceDrop.ts create mode 100644 web_platform/src/app/websiteConfig.test.tsx create mode 100644 web_platform/src/app/workbenchCommands.test.ts create mode 100644 web_platform/src/app/workbenchCommands.tsx create mode 100644 web_platform/src/project/workspaceManifest.test.ts create mode 100644 web_platform/src/project/workspaceManifest.ts create mode 100644 web_platform/src/simulation/SimulationSession.snapshot.test.ts create mode 100644 web_platform/src/simulation/simulationTypes.ts create mode 100644 web_platform/src/simulation/snapshotReaders.ts create mode 100644 web_platform/src/training/LocalTrainingPanel.characterization.test.tsx create mode 100644 web_platform/src/training/TrainingFields.tsx create mode 100644 web_platform/src/training/TrainingJobSection.test.tsx create mode 100644 web_platform/src/training/TrainingJobSection.tsx create mode 100644 web_platform/src/training/TrainingTaskFields.tsx create mode 100644 web_platform/src/training/trainingForm.test.ts create mode 100644 web_platform/src/training/trainingForm.ts create mode 100644 web_platform/src/training/trainingPresentation.ts create mode 100644 web_platform/src/viewer/MuJoCoViewer.mesh.test.ts create mode 100644 web_platform/src/viewer/geometryFactory.ts create mode 100644 web_platform/src/viewer/meshPresentation.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d84614..53688d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,153 @@ ## [未发布] +## [1.0.5] - 2026-09-29 + +### 网站发布与安全精简 + +- 版本号与锁文件根包元数据同步为 `1.0.5`,不升级依赖;包含下述完整安全精简及网站导入选项清理。 +- 网站已发布 `20260929T014129Z-v1-0-5`,上一版本 `20260928T035026Z-import-options` 保留为回滚目标;白名单发布包校验、离线构建、运行时契约预检、容器健康检查及 OpenResty 配置校验通过。前后端成对切换,访客会话重建,原服务器密钥 stat 与持久预算数据库 inode 保留。 +- 发布前重新通过九项门禁/双构建:前端 156 文件/797 项,后端 166 通过/27 项既有 opt-in 跳过;只更新版本元数据,没有升级依赖。 +- 公网非付费验收 14/14 通过(网站/iframe/导入/会话/Python/预设 8 项,内置机器人与双相机 6 项);28 个线上静态文件匹配发布清单,公网 HTML/主 JS 与本地构建逐字节一致。未调用付费模型、未启动真实训练。 +- 发布证据:`build/website-deployment/v1.0.5/`;本地门禁:`build/code-simplification/release-v1.0.5/`。 + +### 代码安全精简(2026-09-28) + +#### 最终交付汇总 + +全部 14 个计划阶段完成;基线为 `7ebe9092ba29a6674d6f5c4153f31214dd6f2319`。以下规模与限制记录截至精简计划验收时(当时未提交、打标签或部署),不含后续版本发布元数据和文档更新;发布结果见本版本条目。 + +| 宿主文件(`web_platform/src/`) | 基线行数 | 最终行数 | 宿主减少 | +| --- | ---: | ---: | ---: | +| `app/App.tsx` | 3272 | 1217 | 2055 | +| `simulation/SimulationSession.ts` | 1417 | 1210 | 207 | +| `viewer/MuJoCoViewer.ts` | 1391 | 1267 | 124 | +| `training/LocalTrainingPanel.tsx` | 1178 | 807 | 371 | +| **合计** | **7258** | **4501** | **2757** | + +- **迁移与新增边界**:30 个新增生产模块,共 4897 行,包含迁移实现、导入、显式类型/端口、Hook 依赖与组合代码,不是净删除量。App 新增 16 个领域/界面 Hook、3 个组合组件和命令组件;项目清单辅助 1 个,Session 类型/采集模块 2 个,Viewer 几何/展示模块 2 个,训练表单/展示模块 5 个。 +- **实际清理**:删除 4 个已证实无用声明,相关生产代码净减 23 行;另将 3 个内部辅助函数取消 export。数值算法、公开兼容类型、协议/Golden 和真实扩展入口未删。移除 App 已无用途的全文件依赖检查豁免,没有新增豁免。 +- **净变化口径**:计入 Git 跟踪变更和本次未跟踪源码/测试;生产代码净增 **2117 行**(不是净删 2757 行),测试净增 **3096 行**(19 个新测试文件 3093 行,既有 E2E 追加 3 行断言)。文档净增 **137** 行,全仓净增 **5350** 行;忽略目录 `plans/`、`build/` 及生成产物不计入。逐文件数据见 `build/code-simplification/final-size-report.json`。 +- **模块边界**:资源/ref 单一所有者;地图交互与创作文件事务分开,避免万能编辑 Hook;没有新全局 store。App 保留显式领域接口和布局,最终 1217 行高于参考目标,未为行数强行压缩或引入共享万能 context。 +- **验证**:前端 156 文件/797 项(较基线新增 19 文件/117 项),后端 166 项通过;typecheck、TS/Python lint、双构建、契约哈希及 diff 检查通过。生产运行时依赖图 239 模块/540 条边,无循环。Session 101 / Viewer 131 个非提取成员逐字未变,物理时钟/控制权/几何资源释放保留原实现。 +- **浏览器验收**:计划 63 项全部通过:本地 App/布局 41,网站 5,内置机器人生产构建/开发 StrictMode 各 6,语言控制 5。双相机的既有姿态阈值未变,新增切换/隐藏后时间仍为 0.000 s 的断言。另用独立本地浏览器专项确认工具 tab 与侧栏隐藏/显示保留训练表单状态且没有 API/训练连接请求。 +- **模式隔离**:网站无训练/外控入口及 tuning HTML/chunk,本地均保留;两者保留 Python、ONNX、语言控制、导入和相机功能。业务 MODE 判断仍只在 `websiteConfig.ts`,Vite 构建判断未改,无新增模式框架。 +- **跳过/限制**:训练服务 24 项既有 opt-in 跳过(4 项真实环境/GPU smoke 或 rollout、10 项需显式预训练源、9 项真实单文件探针、1 项 CPU runner);决策服务 3 项原生 Codex opt-in 跳过。未启用上述开关、未访问生产/付费上游、未启动真实训练;不把本次结构验收当成所有物理任务收敛证明。浏览器仅用进程级文件轮询绕过系统 inotify 额度,未修改系统限制;保留既有第三方 externalization/大 chunk 构建警告,最终前端 lint 无警告。 +- **证据**:最终门禁 `build/code-simplification/final-verification-verified/`;契约/成员/依赖核验 `final-verification/`;完整浏览器日志与专项截图 `final-browser/`。原始失败与修正后的日志均保留,未放宽门禁、协议或既有数值容差。 + +#### 分阶段记录 + +- 完成步骤 13:最终九项门禁/双构建通过,前端 156 文件/797 项,后端 166 通过/27 既有 opt-in 跳过;计划浏览器验收 63 项全部通过(本地 41、网站 5、内置生产/开发各 6、语言 5)。内置相机额外断言切换/隐藏后时间仍为 0.000 s,两套均复验通过;既有相机数值容差未变。12 份契约哈希一致,Session 101 / Viewer 131 个非提取成员逐字未变,239 个生产模块的运行时依赖图无循环。保留构建既有第三方 externalization/大 chunk 警告,未启用真实 GPU/训练/上传/Codex opt-in。证据:`final-verification-verified/`、`final-verification/`、`final-browser/`。 + +- 完成步骤 12:新增 4 项真实 MODE 配置矩阵测试,验证网站禁用训练/外控但保留 Python、ONNX、语言控制、URDF profile/相机增强且不自动联网。定向 17 项、全前端 156 文件/797 项及全部门禁通过;后端 166 通过/27 既有跳过。产物断言确认本地有 index/tuning HTML 与调参 chunk,网站只有 index 且无调参 chunk;业务 MODE 判断仍仅 `websiteConfig.ts`,配置/Vite 未修改。证据:`mode-matrix/`。 + +- 完成步骤 11:`WorkbenchTools.tsx` 稳定组合原 lazy 面板及 agent/外控节点,访问历史仍由 App 持有,未引入新 store。定向 45 项、全前端 155 文件/793 项及九项门禁/双构建通过,后端 166 通过/27 既有跳过;真实 WASM 主工作台预设加载、语言目标和连续自由坐标抓放通过(仅回环假上游)。App 当前 1217 行;保留显式共享 ref/领域端口和布局,不为参考行数目标冒险压缩。证据:`app-workbench-tools/`。 + +- 提取 `useAgentScene.ts` 的预设加载、身份桥接与草稿失效 effect;失效 effect 单独在原位置调用,保持 Viewer effect 次序和取消/确认逻辑。定向 51 项、全前端 155 文件/793 项及全部门禁通过,后端 166 通过/27 既有跳过。现有工具挂载/agent 不随首个 snapshot 重挂载表征亦通过,可作为工具组合提取基线。证据:`app-agent-scene/`。 + +- agent 提取前补充取消/机器人身份、拒绝工程替换不下载、任务会话身份及配置重置 3 项表征;定向 51 项、全前端 155 文件/793 项及九项门禁/双构建通过,后端 166 通过/27 既有跳过。证据:`app-agent-characterization/`。 + +- 提取 `usePolicyWorkflow.ts`:ONNX/移动策略、训练场景导出与导航重置桥接;保留双锁、SHA-256/大小校验、会话身份检查、暂停恢复及成功后缓存顺序。定向 38 项、全前端 155 文件/790 项及九项门禁/双构建通过,后端 166 通过/27 既有跳过。证据:`app-policy-workflow/`。 + +- 策略桥接提取前新增成功后缓存、加载锁/暂停恢复、扩展名/64 MiB 上限 3 项表征。测试使用有效的无部署元数据 ModelProto 字段及假运行时,不修改协议 fixture;定向 36 项、全前端 155 文件/790 项和全部门禁通过,后端 166 通过/27 既有跳过。证据:`app-policy-characterization-verified/`。 + +- 提取 `usePythonController.ts`:脚本缓存/校验、运行时进度、状态互通及报错上下文不变。定向 36 项、全前端 155 文件/787 项及全部九项门禁/双构建通过,后端 166 通过/27 既有跳过;真实 WASM Python 导入启用、持续步进/重载 2 项通过。证据:`app-python-controller/`。 + +- Python 桥接提取前补充失败缓存/路径重试/启用移除及缺失路径/扩展名上下文 2 项测试;定向 27 项、全前端 155 文件/787 项及九项门禁/双构建通过,后端 166 通过/27 既有跳过。证据:`app-python-characterization/`。 + +- 提取 `useSimulationActions.ts`,手动/遥测操作继续委托原 adapter,agent 重置仍按会话身份转交预设加载,不新增时钟。定向 38 项、全前端 155 文件/785 项和九项门禁/双构建通过;后端 166 通过/27 既有跳过。证据:`app-simulation-actions/`。 + +- 手动/遥测提取前新增 3 项表征:暂停/单步/重置/速度次序、参数拒绝不刷新源码、遥测配置/录制操作;完善测试 Props 与参数后,定向 34 项、全前端 155 文件/785 项及九项门禁/双构建通过,后端 166 通过/27 既有跳过。证据:`app-actions-characterization-verified/`。 + +- 提取 `useMapAuthoring.ts` 的场景文件创建、地图转换/导出与编辑提交入口;5 个函数经语法树词法叶节点比较,在 ref/action 别名、动态导入路径及格式归一化后与 HEAD 相同。移除未使用的 Viewer 参数;稳定 setter 单独列为 Hook 依赖,消除新增依赖警告,并移除 App 已无用途的整文件 `exhaustive-deps` 豁免。定向 37 项、全前端 155 文件/782 项及九项门禁/双构建通过(前端 lint 无警告);后端 166 项通过/27 项既有跳过;创建拖放、重力承载、只读转换、V3 编辑、首次放弃 5 项真实 WASM 测试通过。App 1826 → 1595 行;证据:`build/code-simplification/app-map-authoring-verified/`。步骤 11 的控制/桥接与工具组合、步骤 12–14 仍未完成。 + +- 提取 `useMapEditor.ts` 的交互/预览绑定、选择、放置模式、草稿回调及高度查询;四组 effect 保留原次序与身份检查,15 个迁移动作与 HEAD 的参数/函数体在 ref/格式归一化后相同。定向 37 项、全前端 155 文件/782 项及九项门禁/双构建通过;后端 166 项通过/27 项既有跳过;资产创建拖放、属性联动、连续地形拖放、V3 编辑及只读地图转换 5 项真实 WASM 测试通过。App 2018 → 1826 行;证据:`build/code-simplification/app-map-editor/`。创作文件事务另行提取,避免形成万能编辑 Hook。 + +- 地图编辑交互提取前新增 3 项表征:待绑定资产/选择只转交一次、草稿复制及加载锁保护、视口最新实例变换与卸载解绑。补齐测试中的 ViewerSelection 类型后,定向 28 项、全前端 155 文件/782 项及全部门禁通过;后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-map-editor-characterization-verified/`。 + +- 提取 `useMapScene.ts` 的实例放置、共享预览、提交/放弃事务;复用原草稿/清单领域函数,不向编辑 Hook 反向依赖。18 个迁移函数中 17 个与 HEAD 的参数/函数体在 ref 命名及格式归一化后相同,另一读取函数沿用步骤 9 已验证的清单辅助。定向 36 项、全前端 155 文件/779 项及九项门禁/双构建通过;后端 166 项通过/27 项既有跳过;4 项真实 WASM 地图回滚/编辑测试通过。App 2324 → 2018 行;证据:`build/code-simplification/app-map-scene/`。 + +- 步骤 11 地图事务提取前新增 5 项 App 表征:首次场景放弃的 provisional 文件清理、失败清单恢复/显式重试、提交期间新草稿及历史保留、未引用 provisional 剔除、原生 URDF 拒绝地图注入。定向 27 项、全前端 155 文件/779 项及九项门禁/双构建通过,后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-map-scene-characterization/`。 + +- 完成步骤 10:提取 `useWorkspaceDrop.ts`,保留手势内同步获取文件句柄、共享导入锁、地图库优先、世界落点和错误清理;新增 6 项嵌套拖放、提示及三类地图库委托测试。定向 29 项、导入器补充定向 16 项、全前端 155 文件/774 项及全部九项门禁/双构建通过;后端 166 项通过/27 项既有跳过,契约哈希和 `git diff --check` 通过。证据:`build/code-simplification/app-workspace-drop/`。App 累计 3272 → 2324 行,四个宿主合计 7258 → 5608 行;这是迁移缩减,不是全仓净删除。步骤 11–14 和最终完整浏览器验收仍未完成,未提交版本。 + +- 拖放提取前新增 3 项表征:手势内同步扫描/重复锁、扫描失败恢复、地图库优先和世界落点;URDF 回退断言等待实际异步 UI 完成,而不只等待较早的 `loading=false`。定向 14 项、全前端 154 文件/768 项及全部九项门禁/双构建通过;后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-drop-characterization-verified/`;原失败日志保留,未放宽门禁或数值容差。 + +- 提取 `useProjectImport.ts`:文件选择、导入进度、URL 下载/取消、共享锁、工程关闭与资源清理;不新建共享状态容器。定向 38 项、全前端 154 文件/765 项及全部九项门禁/双构建通过,后端 166 项通过/27 项既有跳过;开发态 StrictMode 内置机器人 URL 自动导入/刷新及双相机真实 WASM 验证通过。证据:`build/code-simplification/app-project-import/`。 + +- 工程导入提取前新增 4 项 App 表征:StrictMode URL 单次下载/手动锁竞争、卸载后迟到下载、下载失败后手动恢复及关闭工程清理。定向 29 项、全前端 154 文件/765 项及全部九项门禁/双构建通过,后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-import-characterization/`。 + +- 提取 `useModelLoading.ts` 的模型替换事务、URDF 确认/跳过、profile/base 回退;ref 单一所有权、锁和 attach/rollback/release 次序不变。新增 4 项取消/外控失败回归,清理迁移后未使用导入;定向 22 项、全前端 154 文件/761 项及全部九项门禁/双构建通过,后端 166 项通过/27 项既有跳过;真实 WASM 源码重载、URDF 增强、失败保留会话另有 3 项通过。证据:`build/code-simplification/app-model-loading-verified/`。 + +- 原 App 加载事务新增 7 项表征:成功顺序、失败保留入口/暂停态/源码、attach 回滚、视觉警告非致命、加载锁、applied/草稿隔离、URDF 确认回退与跳过。修正测试替身自动 mock 的原型方法覆盖及类型装配后,定向 16 项、全前端 153 文件/757 项和全部九项门禁/双构建通过;后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-loading-characterization/`。 + +- Viewer 设置同步收拢至生命周期模块,按原顺序保留“显示设置 → 场景/agent effect → 相机/主题”两个设置组;不增加时钟。新增次序/吸附回归,定向 10 项、全前端 152 文件/750 项及全部九项门禁/双构建通过,后端 166 项通过/27 项既有跳过。证据:`build/code-simplification/app-viewer-settings/`。 + +- 提取 `useViewerLifecycle.ts` 的异步初始化/初始设置与资源解绑,保留 active 取消、viewerReady 身份检查、retired adapter 释放/替换顺序和首轮动作闭包;帧/选择回调及后续设置 effect 暂留 App,避免改变业务 effect 顺序。新增 2 项迟到初始化取消/构造失败表征;定向 13 项、全前端 151 文件/749 项及全部门禁/双构建通过;后端 166 项通过、27 项既有跳过。开发态 StrictMode URL 导入/刷新和真实 WASM 双相机位置/朝向验收另有 1 项通过。证据:`build/code-simplification/app-viewer-initialization/`。 + +- 提取 `useMapWorkspaceState.ts` 的地图状态/ref 与草稿派生视图,保留事务显式同步 ref 的时机,不将 UI setter 自动应用到物理基线。新增 4 项默认值/基线分离/计数/跨实例选择测试;定向 19 项、全前端 151 文件/747 项及全部门禁/双构建通过;后端 166 项通过、27 项既有跳过。真实 WASM 的首次场景放弃、共享草稿/回滚、失败保留会话另有 3 项通过(测试进程使用文件轮询)。证据:`build/code-simplification/app-map-state/`。 + +- 阶段 2.5 补充真实 WASM 浏览器验收:平台骨架/单模型、源码重载、未保存确认、URDF 驱动器/摄像头及无效模型诊断共 5 项通过。首次因系统 inotify 监听额度 `ENOSPC` 无法启动;仅为测试进程设置 `CHOKIDAR_USEPOLLING=1 CHOKIDAR_INTERVAL=1000` 后成功,未改系统参数或测试标准。证据:`build/code-simplification/app-low-risk-browser-polling/`;原始环境失败日志保留。 + +- 提取 `useProjectSource.ts`,保留 profile/缓存保存/通知/加载顺序、URDF 与 MJCF/ZIP 导出和不同错误上下文;共享清单 ref 不另建副本。新增 3 项回归;按 lint 提示显式命名 `manifestRef`,无新增规则豁免。阶段 2.5 完成,`App.tsx` 暂由 3272 降至 2946 行;定向 17 项、全前端 150 文件/743 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/app-project-source-verified/`。 + +- 提取 `project/workspaceManifest.ts` 的缓存路径、URDF link 名称、provisional 文件剔除与地图 authoring 读取,沿用现有地图解析器、不缓存可变文档。新增 4 项回归;定向 18 项、全前端 149 文件/740 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-manifest-helpers/`。 + +- 提取 `WorkbenchDialogs.tsx`,仅组合既有根级弹窗及命令面板;源码编辑器继续 lazy,URDF 按路径重置,入口选择仍留在原视口内。复用原组件 Props,未新建全局状态;定向 19 项、全前端 148 文件/736 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-dialogs/`。 + +- 提取 `WorkspaceNotices.tsx` 组合现有错误恢复/Toast,保留错误优先级、按诊断时间重置展开,以及“仅携带路径的模型编译错误可重试”。新增 2 项回归;定向 13 项、全前端 148 文件/736 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-notices/`。 + +- 提取 `useWorkbenchFeedback.ts`,编译/导入/普通通知共享一个稳定发布入口,消除重复 ID、20 条封顶与 Toast 插入;类别、文本、时间及诊断构造不变。新增 3 项通知/诊断测试;定向 14 项、全前端 147 文件/734 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-feedback/`。 + +- 将命令目录提取至 `workbenchCommands.tsx`,以无状态 `WorkbenchCommandPalette` 组合原面板;命令 ID/标签/禁用规则不变。最初纯工厂调用触发 React refs/purity 检查,改用明确组件 Props 边界,不增加 lint 豁免;新增 3 项目录/状态/回调测试,证明渲染不执行动作。定向 14 项、全前端 146 文件/731 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-commands-verified/`。 + +- 提取 `useWorkbenchChrome.ts` 的界面状态、全屏与布局动作,组合已有主题/侧栏 Hook;显示偏好持久化仍在原 Viewer 同步 effect,不改变物理资源所有权。新增 3 项布局/工具/全屏清理测试;定向 17 项、全前端 145 文件/728 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-chrome/`。 + +- 提取 `useWorkbenchShortcuts.ts`,复用 `keyboard.ts`;不改变逐次渲染监听、大小写、repeat、输入/模态规则。新增 3 项与现有地图 capture 快捷键的组合测试,确认 R 缩放不触发物理重置、动作更新和卸载清理。定向 21 项、全前端 144 文件/725 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-shortcuts/`。 + +- App 原实现新增 7 项编排表征:StrictMode 有效 Viewer/卸载、快捷键新闭包与输入/模态优先级、显示偏好损坏回退、工具/agent 挂载稳定及禁止自动网络请求。修正测试替身的缺失方法,并改用自动 mock 避开 Vitest 手动 factory 的并发 import 限制;未修改 App 实现,初次失败日志保留。定向 7 项、全前端 143 文件/722 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/app-characterization-final/`。 + +- 提取 `meshPresentation.ts` 的矩阵、材质与高亮更新,保留碰撞颜色/深度/透明规则、粗糙度及关节高亮优先级。阶段 2.4 完成:`MuJoCoViewer.ts` 1391 → 1267 行;AST 复核 131 个非提取成员与 HEAD 原文一致,缓存/释放、渲染调度、DPR、阴影与传感器相机不变。定向 12 项、全前端 142 文件/715 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/viewer-mesh-presentation/`。 + +- 提取 `geometryFactory.ts`,复用现有 heightfield/mesh-id helper,保留胶囊算法、细分与 typed-array 批量复制;几何/纹理缓存和释放继续归 Viewer 所有。定向 10 项、全前端 142 文件/715 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/viewer-geometry-factory/`。 + +- Viewer 提取前新增 7 项表征,锁定 primitive/胶囊细分、mesh/heightfield 数组、矩阵排列、凸包 dataid、碰撞高亮及共享/动态资源释放。定向 12 项、全前端 142 文件/715 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/viewer-mesh-characterization/`。 + +- Session 快照 actuator/joint/body 只读采集提取至 `snapshotReaders.ts`,保留数值表达式、数组顺序、单位、默认名和句柄 finally 清理。阶段 2.3 完成:`SimulationSession.ts` 1417 → 1210 行;AST 逐方法复核确认另外 69 个方法/构造器/getter 与 HEAD 原文一致。定向 22 项、全前端 141 文件/708 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/simulation-snapshot-readers/`。 + +- 在原 `SimulationSession.snapshot()` 上新增 7 项表征,覆盖四种 actuator 类型与阈值、多通道、限位显示、控制权只读、数组复制、不步进/不写物理数组以及嵌套异常释放句柄。定向 18 项、全前端 141 文件/708 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/simulation-snapshot-characterization/`。 + +- 将 Session 公共类型原样迁移至 `simulationTypes.ts`,通过原模块 type re-export 保持所有既有导入路径;关节限位内部类型命名复用,未改任何仿真方法。定向 22 项、全前端 140 文件/701 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/simulation-types/`。 + +- 训练任务默认值集中至 `trainingForm.ts`,新增 3 项 Go2/LeKiwi 默认参数回归,保留任务切换写入次序。阶段 2.2 完成:`LocalTrainingPanel.tsx` 从 1178 行降至 807 行(迁移/拆分 371 行,非净删除);5 个辅助/展示模块共 619 行,连接 epoch、1500ms 轮询和协议生命周期仍在原面板。定向 33 项、全前端 140 文件/701 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/training-task-defaults/`。 + +- 提取 `TrainingJobSection.tsx`:作业状态、日志、评估和成果展示与父级异步操作分离,继续训练/新建的状态写入仍由原面板持有。新增 3 项操作边界测试;定向 24 项、全前端 140 文件/698 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-job-section/`。 + +- 提取 `TrainingTaskFields.tsx` 的受控地形/传感器与移动任务配置区,沿用已有训练类型和父组件状态,保留折叠后输入、默认范围与任务切换。定向 21 项、全前端 139 文件/695 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-task-fields/`。 + +- 原样提取训练 `Field/NumberField` 至 `TrainingFields.tsx`,保留输入 Number 转换、范围/步长、标签与 Tooltip。定向 21 项、全前端 139 文件/695 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-fields/`。 + +- 提取 `trainingForm.ts` 的 GPU 解析、参数校验和请求构造,保留可选字段、校验顺序、移动任务 W&B/sourceJobId 行为及场景编译/上传顺序。新增 9 项纯函数测试;定向 30 项、全前端 139 文件/695 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-form/`。 + +- 提取训练展示辅助至 `trainingPresentation.ts`,原样保留活动状态、作业中文标签及地形/参数标签。定向 21 项、全前端 138 文件/686 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-presentation/`。 + +- 训练面板拆分前新增 6 项请求表征,锁定完整 Flat 请求、CPU 空 GPU 列表、GPU 分隔/重复编号及非法输入不提交行为;原实现未改。修正新测试的 Testing Library 选项类型后,定向 21 项、全前端 138 文件/686 项和全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过。证据:`build/code-simplification/training-characterization-verified/`(首次失败日志另保留)。 + +- 原子收窄 `controller/PythonControllerRuntime.ts` 的 `getPythonRuntime` 导出面,保留 Pyodide 初始化与控制调度。定向 13 项、前端 680 项及全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。阶段 2.1 六个文件全部独立验收,生产代码净减 23 行。证据:`build/code-simplification/cleanup-python-loader-export/`。 + +- 原子收窄 `simulation/PhysicsAdapter.ts` 的 `getMujocoModule` 导出面,WASM 初始化缓存、失败重试及加载事务不变。定向 8 项、前端 680 项及全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/cleanup-mujoco-loader-export/`。 + +- 原子收窄 `project/cachedFiles.ts` 的 `isEditableSource` 导出面,保留其文件内调用和实现。定向 6 项、前端 680 项及全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/cleanup-cached-files-export/`。 + +- 原子清理 `tuning/domain.ts`:删除无引用的 `latestCompletedTrial` 及专属类型导入,奖励参数、目标权重与状态目录不变。定向 6 项、前端 680 项及全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/cleanup-tuning-domain/`。 + +- 原子清理 `tuning/ScalarRingBuffer.ts`:移除无调用的 `snapshotSeries`、专属 `BufferedMetricSeries` 及多余类型导入,不改环形缓冲实现。定向 6 项、前端 680 项及全部前后端门禁/双构建通过;后端 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/cleanup-scalar-ring-buffer/`。 + +- 原子清理 `app/editorSelection.ts`:删除无引用的 `isMapEditorSelection`,保留全部选择类型。定向 8 项、前端 680 项及全部前后端门禁/双构建通过;后端仍为 166 项通过、27 项既有跳过,契约哈希不变。证据:`build/code-simplification/cleanup-editor-selection/`。 + +- 完成四个核心大文件的只读审查与分阶段计划审批。重构前工作区干净,12 个契约文件与 HEAD 完全一致;未修改物理算法、控制频率、Schema 或 Golden。 +- 原始基线全部通过:typecheck、前端 lint、137 文件/680 项前端单测、本地/网站双构建、Python lint;控制桥接 16 项、决策服务 48 项、训练服务 102 项通过,另有决策服务 3 项及训练服务 24 项既有可选/环境测试跳过。构建第三方 externalization/大型 chunk 和 Python 弃用警告保留。日志与源码/契约 SHA-256 位于 `build/code-simplification/baseline-20260928T055602Z/`;未部署或调用付费模型。 + ### 网站导入选项清理(2026-09-28) - 网站版 URDF/ZIP 导入弹窗移除 LeKiwi v1、LeKiwi Bundle 的“移动操作训练场景”选项及训练提示;不沿用已移除的初始训练选择。保留通用转换、普通 LeKiwi profile、语言控制与本地版训练功能。 diff --git a/docs/website-deployment.md b/docs/website-deployment.md index ae2cbe9c..2a3eb76d 100644 --- a/docs/website-deployment.md +++ b/docs/website-deployment.md @@ -2,7 +2,7 @@ 目标:`https://cadworld-sim.robotquan.com`,主机 `root@47.93.31.109`。复用 1Panel 的 OpenResty,独立容器仅发布 `127.0.0.1:8768`。不安装训练/GPU 服务,不改系统 Python,不需要访问者开启终端。 -当前在线版本:`20260928T035026Z-import-options`,上一版本 `20260924T081555Z-language-render-fix`。网站 URDF/ZIP 导入弹窗已移除两项“移动操作训练场景”及训练提示,普通导入、语言控制和本地训练保留。沿用原后端/代理配置,通过现有成对发布流程上线;后端容器已重启,旧访客会话失效,密钥文件与持久预算目录保持不变。公网非付费检查 8/8 通过(含两种文件导入、预设/双相机、Python、会话安全和 iframe),HTML/主 JS 与构建产物一致;未调用付费模型。证据:`build/website-deployment/import-options-20260928/`。此前渲染与语言监督修复详见 [修复与发布记录](website-language-render-fix.md)。 +当前在线版本:`20260929T014129Z-v1-0-5`(Git `v1.0.5`),上一版本 `20260928T035026Z-import-options`。本次发布 App/Session/Viewer/训练面板安全拆分与冗余清理;网站 URDF/ZIP 导入仍不提供移动操作训练场景,普通导入、语言控制、Python/ONNX 与双相机保留,本地训练/外控不受影响。沿用原后端/代理配置,通过现有成对发布流程上线;后端容器已重建,旧访客会话失效,密钥 stat 与持久预算数据库 inode 保持不变。发布前九项门禁/双构建通过(前端 797 项、后端 166 通过/27 项既有跳过);公网非付费检查 14/14 通过(网站/iframe/导入/会话/Python/预设 8 项,内置机器人 6 项),28 个静态文件匹配发布清单,公网 HTML/主 JS 与构建产物一致;未调用付费模型或启动真实训练。证据:`build/website-deployment/v1.0.5/`。此前渲染与语言监督修复详见 [修复与发布记录](website-language-render-fix.md)。 iframe 嵌入已通过响应头热更新放行 `https://cadworld.robotquan.com`,未重启应用;接入示例、验证范围与配置回滚见 [嵌入说明](website-embedding.md)。重新发布旧包会覆盖该热更新,应从当前源码重新构建。 diff --git a/package-lock.json b/package-lock.json index be1b044f..32ea560d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mujoco-web-platform", - "version": "1.0.3", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mujoco-web-platform", - "version": "1.0.3", + "version": "1.0.5", "license": "Apache-2.0", "dependencies": { "@monaco-editor/react": "^4.7.0", diff --git a/package.json b/package.json index 8ca7466e..675b473b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mujoco-web-platform", - "version": "1.0.3", + "version": "1.0.5", "description": "基于 MuJoCo WebAssembly 的本地机器人仿真与控制平台", "private": true, "type": "module", diff --git a/web_platform/e2e/builtinRobots.spec.ts b/web_platform/e2e/builtinRobots.spec.ts index 64cecf89..8639f415 100644 --- a/web_platform/e2e/builtinRobots.spec.ts +++ b/web_platform/e2e/builtinRobots.spec.ts @@ -84,6 +84,8 @@ test('URL 自动下载、配置 profile、加载真实 WASM;刷新可重入且 await expect(page.getByRole('button', { name: '关节拖动', exact: true })).toBeDisabled(); expect(downloads).toHaveLength(1); await verifyBuiltinCameras(page, info); + const simulationTime = page.getByLabel('视口状态').getByText(/^时间 /); + await expect(simulationTime).toHaveText('时间 0.000 s'); const selector = page.getByRole('combobox', { name: '摄像头视角' }); const frame = page.getByLabel('摄像头画面', { exact: true }); await expect(selector.locator('option')).toHaveCount(2); @@ -104,6 +106,7 @@ test('URL 自动下载、配置 profile、加载真实 WASM;刷新可重入且 await expect(frame).toHaveAttribute('data-camera-body', 'Camera-Model-v3-1'); await expect(play).toBeEnabled(); expect(downloads).toHaveLength(1); // Switching/hiding never reloads the robot. + await expect(simulationTime).toHaveText('时间 0.000 s'); // 相机选择/隐藏不推进物理。 await page.reload(); await expect(play).toBeEnabled({ timeout: 90000 }); await expect(page.getByRole('dialog', { name: '配置 URDF 仿真组件' })).toHaveCount(0); diff --git a/web_platform/src/app/App.characterization.test.tsx b/web_platform/src/app/App.characterization.test.tsx new file mode 100644 index 00000000..f7d5c846 --- /dev/null +++ b/web_platform/src/app/App.characterization.test.tsx @@ -0,0 +1,232 @@ +import { StrictMode, useEffect, type ReactNode } from 'react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import type { MuJoCoViewer } from '../viewer/MuJoCoViewer'; +import type { SimulationSnapshot } from '../simulation/SimulationSession'; +import { DEFAULT_VIEWER_DISPLAY_OPTIONS } from '../viewer/displayOptions'; +import { useAppStore } from '../stores/useAppStore'; +import { App } from './App'; + +type Callbacks = ConstructorParameters[1]; +const probe = vi.hoisted(() => ({ + viewers: [] as { + callbacks: Callbacks; + display: ReturnType; + dispose: ReturnType; + }[], + setPaused: vi.fn(), + reset: vi.fn(), + adapterDispose: vi.fn(), + agentMount: vi.fn(), + agentUnmount: vi.fn(), +})); +vi.mock('../simulation/PhysicsAdapter', () => ({ + MainThreadPhysicsAdapter: class { + session = null; + snapshot = () => null; + dispose = probe.adapterDispose; + setPaused = probe.setPaused; + reset = probe.reset; + externalControlStatus = () => undefined; + stopExternalControl = vi.fn(); + }, +})); +// 自动 mock 避开 Vitest 手动 factory 对并发 import 的 callstack 限制。 +vi.mock('../viewer/MuJoCoViewer'); +class ViewerDouble { + constructor(_host: HTMLElement, callbacks: Callbacks) { + probe.viewers.push({ callbacks, display: this.setDisplayOptions, dispose: this.dispose }); + } + dispose = vi.fn(); + setDisplayOptions = vi.fn(); + setMode = vi.fn(); + setMapDisplay = vi.fn(); + setShowSensorCamera = vi.fn(); + setSensorCameraViewportElement = vi.fn(); + setOrientationGizmoHost = vi.fn(); + setTheme = vi.fn(); + setParametricMapAssets = vi.fn(); + setMapEditorTransformMode = vi.fn(); + setMapEditorSnapping = vi.fn(); + setShowPerceptionRays = vi.fn(); + setMapEditorPreviewInstances = vi.fn(); +} +vi.mock('./components/WorkspaceToolsPanel', () => ({ + WorkspaceToolsPanel: ({ agentTask }: { agentTask: ReactNode }) => <>{agentTask}, +})); +vi.mock('../mobile/agent/AgentTaskPanel', () => ({ + AgentTaskPanel: function ProbeAgent() { + useEffect(() => { + probe.agentMount(); + return () => { + probe.agentUnmount(); + }; + }, []); + return
; + }, +})); + +const snapshot: SimulationSnapshot = { + time: 0, + qpos: [], + qvel: [], + ctrl: [], + actuators: [], + joints: [], + bodies: [], + warnings: [], + model: { nbody: 0, njnt: 0, ngeom: 0, ncam: 0, nactuator: 0, nu: 0, nq: 0, nv: 0 }, + telemetry: { + recording: false, + limitReached: false, + sampleCount: 0, + segmentCount: 0, + config: { bodyId: 0, sampleRateHz: 10, maxSamples: 100 }, + body: { id: 0, name: 'world' }, + summary: { + duration: 0, + distanceHorizontal: 0, + maxHorizontalSpeed: 0, + maxAbsRoll: 0, + maxAbsPitch: 0, + }, + }, +}; +async function ready() { + await waitFor(() => expect(probe.viewers).toHaveLength(1)); +} +beforeEach(async () => { + const { MuJoCoViewer: Viewer } = await import('../viewer/MuJoCoViewer'); + vi.mocked(Viewer).mockImplementation(ViewerDouble as unknown as typeof MuJoCoViewer); + vi.clearAllMocks(); + probe.viewers.length = 0; + localStorage.clear(); + useAppStore.setState(useAppStore.getInitialState(), true); + window.history.replaceState({}, '', '/'); + vi.stubGlobal( + 'fetch', + vi.fn(() => Promise.reject(new Error('此表征测试禁止网络访问'))), + ); +}); +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('App 原实现的编排边界', () => { + it('动态初始化完成前卸载不会创建迟到 Viewer', async () => { + const { unmount } = render(); + unmount(); + await vi.dynamicImportSettled(); + expect(probe.viewers).toHaveLength(0); + expect(probe.adapterDispose).toHaveBeenCalledOnce(); + }); + + it('Viewer 构造失败保留原渲染诊断,资源在卸载时清理', async () => { + const { MuJoCoViewer: Viewer } = await import('../viewer/MuJoCoViewer'); + vi.mocked(Viewer).mockImplementationOnce( + class { + constructor() { + throw new Error('模拟初始化失败'); + } + } as unknown as typeof MuJoCoViewer, + ); + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + try { + const { unmount } = render(); + await waitFor(() => + expect(useAppStore.getState().diagnostic).toMatchObject({ + category: '渲染', + summary: '渲染失败', + detail: '模拟初始化失败', + }), + ); + expect(probe.viewers).toHaveLength(0); + unmount(); + expect(probe.adapterDispose).toHaveBeenCalledOnce(); + } finally { + error.mockRestore(); + } + }); + + it('StrictMode 只保留一个有效 Viewer,卸载释放并取消键盘监听,无自动网络请求', async () => { + const { unmount } = render( + + + , + ); + await ready(); + expect(probe.viewers[0].dispose).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + unmount(); + expect(probe.viewers[0].dispose).toHaveBeenCalledOnce(); + expect(probe.adapterDispose).toHaveBeenCalledTimes(2); + fireEvent.keyDown(window, { key: 'r' }); + expect(probe.reset).not.toHaveBeenCalled(); + }); + + it('全局 Space 使用最新暂停状态;r 大小写及 repeat 行为不被顺手修改', async () => { + render(); + await ready(); + fireEvent.keyDown(window, { code: 'Space', key: ' ' }); + fireEvent.keyDown(window, { code: 'Space', key: ' ' }); + expect(probe.setPaused.mock.calls.slice(-2)).toEqual([[false], [true]]); + fireEvent.keyDown(window, { key: 'R' }); + expect(probe.reset).not.toHaveBeenCalled(); + fireEvent.keyDown(window, { key: 'r', repeat: true }); + expect(probe.reset).toHaveBeenCalledOnce(); + }); + + it('输入、按钮和已经 preventDefault 的事件不触发全局重置', async () => { + const { container } = render(); + await ready(); + const input = document.createElement('input'); + container.append(input); + fireEvent.keyDown(input, { key: 'r' }); + fireEvent.keyDown(screen.getAllByRole('button')[0], { key: 'r' }); + const event = new KeyboardEvent('keydown', { key: 'r', bubbles: true, cancelable: true }); + event.preventDefault(); + fireEvent(window, event); + expect(probe.reset).not.toHaveBeenCalled(); + }); + + it('按钮上的 Ctrl+K 可打开命令面板,模态输入焦点抑制全局快捷键', async () => { + render(); + await ready(); + fireEvent.keyDown(screen.getAllByRole('button')[0], { key: 'K', ctrlKey: true }); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + const input = screen.getByRole('dialog').querySelector('input')!; + input.focus(); + fireEvent.keyDown(window, { key: 'r' }); + expect(probe.reset).not.toHaveBeenCalled(); + }); + + it.each([ + ['{损坏 JSON', DEFAULT_VIEWER_DISPLAY_OPTIONS], + [ + JSON.stringify({ showCollision: true, showInertia: '错误类型', unknown: true }), + { ...DEFAULT_VIEWER_DISPLAY_OPTIONS, showCollision: true }, + ], + ])('损坏/混合类型的显示偏好安全回退:%s', async (stored, expected) => { + localStorage.setItem('mujoco-platform-display', stored); + render(); + await ready(); + expect(probe.viewers[0].display).toHaveBeenLastCalledWith(expected); + expect(JSON.parse(localStorage.getItem('mujoco-platform-display')!)).toEqual(expected); + }); + + it('工具首次访问才挂载,切换检查器及首个 snapshot 不重挂载 agent', async () => { + const { unmount } = render(); + await ready(); + expect(probe.agentMount).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole('tab', { name: '控制台' })); + await screen.findByTestId('agent-mount-probe'); + expect(probe.agentMount).toHaveBeenCalledOnce(); + fireEvent.click(screen.getByRole('tab', { name: '检查器' })); + expect(probe.agentUnmount).not.toHaveBeenCalled(); + act(() => useAppStore.getState().setSnapshot(snapshot)); + expect(probe.agentMount).toHaveBeenCalledOnce(); + expect(probe.agentUnmount).not.toHaveBeenCalled(); + unmount(); + expect(probe.agentUnmount).toHaveBeenCalledOnce(); + }); +}); diff --git a/web_platform/src/app/App.loading.test.tsx b/web_platform/src/app/App.loading.test.tsx new file mode 100644 index 00000000..09b98930 --- /dev/null +++ b/web_platform/src/app/App.loading.test.tsx @@ -0,0 +1,1004 @@ +import { StrictMode, type ReactNode, type ReactElement } from 'react'; +import type { AgentWorkbenchBridge } from '../mobile/agent/AgentTaskPanel'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { BUILTIN_ROBOTS, downloadBuiltinRobot } from '../project/builtinRobots'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import type { MuJoCoViewer } from '../viewer/MuJoCoViewer'; +import type { PhysicsLoadOptions } from '../simulation/PhysicsAdapter'; +import type { SimulationSnapshot } from '../simulation/SimulationSession'; +import type { ControllerStatus } from '../controller/types'; +import type { RLPolicyStatus } from '../rl/types'; +import { useAppStore } from '../stores/useAppStore'; +import { importBrowserFiles, filesFromDrop } from '../project/importer'; +import { MAP_LIBRARY_DRAG_MIME, encodeMapLibraryDragPayload } from '../map/editor/assetCatalog'; +import type { ProjectManifest } from '../project/types'; +import { ProjectSidebar } from './components/ProjectSidebar'; +import { ModelControlsSidebar } from './components/ModelControlsSidebar'; +import { UrdfImportOptionsDialog } from './components/UrdfImportOptionsDialog'; +import { App } from './App'; +import { WorkspaceToolsPanel } from './components/WorkspaceToolsPanel'; +import type { useMapWorkspaceState } from './hooks/useMapWorkspaceState'; + +const probe = vi.hoisted(() => ({ + events: [] as string[], + action: vi.fn(), + acceptParameters: true, + python: vi.fn<(source: string, path: string) => Promise>(), + policy: vi.fn<() => Promise>(), + load: vi.fn<(path: string, options: PhysicsLoadOptions) => Promise>(), + attach: vi.fn<(path: string | undefined) => void>(), + visual: vi.fn<() => Promise>(), + viewers: [] as ViewerDouble[], + projects: [] as ProjectManifest[], + mapState: null as ReturnType | null, +})); +vi.mock('../project/importer', async (original) => ({ + ...(await original()), + importBrowserFiles: vi.fn(), + filesFromDrop: vi.fn(), +})); +vi.mock('../project/builtinRobots', async (original) => ({ + ...(await original()), + downloadBuiltinRobot: vi.fn(), +})); +vi.mock('./components/ProjectSidebar', () => ({ ProjectSidebar: vi.fn(() => null) })); +vi.mock('./components/WorkspaceToolsPanel', () => ({ WorkspaceToolsPanel: vi.fn(() => null) })); +vi.mock('./components/ModelControlsSidebar', () => ({ + ModelControlsSidebar: vi.fn( + (props: { workspaceTools?: ReactNode }) => props.workspaceTools ?? null, + ), +})); +vi.mock('./components/UrdfImportOptionsDialog', () => ({ + UrdfImportOptionsDialog: vi.fn(() => null), +})); +vi.mock('./components/SourceEditorDialog', () => ({ + SourceEditorDialog: ({ code }: { code: string }) => ( +