# 07 · 开发与测试 > 面向改代码的人(与 agent)。**先读 [02-系统架构](02-系统架构) §六「关键不变量」**, > 再动手改,能省掉大部分回退。 ## 一、工程约定 | 项 | 约定 | | --- | --- | | 语言 | TypeScript **strict**(含 `noUncheckedIndexedAccess`)、ESM、导入写 `.ts` 扩展名 | | 构建 | `tsdown`(rolldown),host/client 双产物;懒加载 chunk 走 `src/client/chunks/` | | 包管理 | `pnpm`(11.x);`pnpm install` 会触发 `prepare` 构建 | | 测试 | `vitest`;默认 node 环境,需要 DOM 的 spec 用 `// @vitest-environment jsdom` | | 宿主 peer | `dsh-better-sidebar`(本仓库已改名)**永不打包**,必须留在 peer/neverBundle 一侧 | | 命名 | 信息层 tab id 前缀 `apes:`;插件名 `dsh-apes-sidebar` | | 颜色 | 只用 `--dsw-alias-*` 设计令牌(`var(--token, <回退>)`),不写死色值 | ## 二、目录结构(`main` 实况) ``` src/ index.ts HOST 入口:name / inject / apply(ctx, config),路由与围栏装配 invariant.ts 零依赖不变量面(host/client 共用) context-types.ts cordis 服务面契约(ctx.sessions / sidebarRight / …) trust-fence.ts 请求信任围栏(Host 头 / 令牌) html-route.ts /sidebar/html 路由(路径围栏 + 体积上限) config.ts 挂载行配置 schema 与默认值(readLimit / mediaLimit / …) fs-*.ts git.ts … 文件系统、Git、会话、任务、终端等 host 能力 client/ index.tsx CLIENT 入口(apply):装配 store/service/native/apes service.ts ctx.betterSidebar 服务实现(对外 API + 能力清单) state.ts 侧边栏状态存储(store) native/ 原生右侧栏适配(tab 记录、tab 体、tab 芯片标题) builtins/ 内置面板:editor / git / subagent / sidechat / terminal / browser + 预览器 sidebar/ 自绘底部工作台与开合控件 apes/ 信息层三 tab(本 fork 增量:tabs.tsx / icons.tsx / locales.ts) changes/ diff/ 文件变动与 diff 视图 chunks/ 懒加载 chunk:editor / locale / mermaid / terminal locales*.ts i18n(`locales.ts` + 19 个语言覆盖文件) *.module.css 组件样式(含 sidebar.module.css:面板宿主层契约) tests/ vitest specs(131 个文件,含 e2e/ 的挂载脚本型用例) scripts/ install / switch / e2e 挂载 / 发布辅助 docs/ external-plugin-guide.md(对外契约)+ plans/(上游历史设计)+ wiki/(本百科源文件) ``` ## 三、门禁(提交前必跑) ```bash pnpm typecheck # tsc --noEmit(strict) pnpm build # rm -rf lib && tsc(d.ts) && tsdown pnpm test # vitest run ``` 当前基线(本次会话实测):`main` = 131 文件 / **1367 通过** + 9 skip; `sync/v0.24.1` = 141 文件 / **1515 通过** + 9 skip。全量 test 约 3–4 分钟。 **任何一条不过就不要提交**;三条全绿才算完成。 ## 四、测试体系(按层次) | 层次 | 环境 | 手法 | 代表 spec | | --- | --- | --- | --- | | 宿主插件形状 / 对外契约 | node | 断言导出面与语义(含与 `package.json` 的版本一致性) | `smoke.spec.ts` · `plugin-shape.spec.ts` · `service.spec.ts` · `consumer-types.ts` | | 原生侧边栏适配 | node | 用「类型为真、运行时为假」的 mock 注册表 | `native-surface.spec.ts` | | 面板宿主与层叠契约 | node + jsdom | CSS 断言 + DOM 挂载点解析 | `panel-host-css.spec.ts` · `panel-host-parent.spec.ts` | | 预览交付 | node + jsdom | 纯逻辑(`` 注入)+ 客户端挂载(stub `fetch`,`createRoot` + `act`) | `html-preview.spec.ts` · `html-preview-frame.spec.tsx` | | 沙箱契约 | node | 对渲染结果做**字符串断言**(服务端渲染把 `srcDoc` 输出为 camelCase `srcDoc=""`,与客户端设置的真实属性 `srcdoc` 不同 —— 断言方式随之不同) | `sandbox-views.spec.tsx` | | 信息层 tab | node | mock `ctx.sessions.list` 与投影 | `apes-tabs.spec.ts` | | 桌面环境探测 | node | stub 全局(URL stamp / preload 标记) | `desktop-env.spec.ts` | 约定: - **不引入 jsdom 到默认环境**:需要 DOM 的 spec 在文件头写 `// @vitest-environment jsdom`。 - **不依赖真实 DSH 宿主**:需要真宿主的用例归到 `tests/e2e/`(脚本型挂载),不进单元门禁。 - 新增「契约」必须**同时新增 pin 它的 spec**(例如本文档提到的 25 层级、沙箱令牌、`srcDoc` 交付方式, 都有对应断言)。改契约 = 改代码 + 改 spec,两者必须一起提交。 ## 五、扩展点(对外服务) 其他插件通过 `ctx.betterSidebar` 接入: ```ts // 注册一个 tab 类型 await ctx.betterSidebar.registerTab(descriptor) // 注册一个文件预览器 await ctx.betterSidebar.registerFileViewer(viewer) ``` - 服务按**能力清单**做前向兼容门控(能力只增不减),消费方应据此判断可用性而非版本号比较。 - 完整契约、字段表与示例见仓库内 [docs/external-plugin-guide.md](../blob/main/docs/external-plugin-guide.md)。 - 改服务面时:**只能新增**;删除或改语义需同步更新能力清单、指南与本百科。 ## 六、上游同步:测量式 re-base(本项目的方法论) **不 merge 上游**。上游每个版本差距可达数百文件,直接合并会引入无关变更并放大冲突面; 本项目改用「先测量、再重建、后补增量」: ### 步骤 **① 测出自己的增量(把改名归一化掉)** ```python # 对每个变更文件:取出两边 blob,把改名归一化后比较 old = git('show', f'{base}:{path}'); new = git('show', f'HEAD:{path}') rename_only = old.replace('dsh-better-sidebar', 'dsh-apes-sidebar') == new ``` → 得到三张清单:**仅改名**(自动处理)/ **真改动** / **新增**。数量级参考:v0.19.1→v0.24.1 为 78 / 11 / 10。 **② 确定改名的精确范围(不要重新推断)** ```bash # 仍保留上游名字的文件 = 有意保留(本仓库为 README.md、package.json、docs/plans/*) comm -12 <(git grep -l 'dsh-better-sidebar' <旧基线> | cut -d: -f2 | sort) \ <(git grep -l 'dsh-better-sidebar' HEAD | cut -d: -f2 | sort) ``` 其余文件只做子串替换 `dsh-better-sidebar` → `dsh-apes-sidebar`;**camelCase 服务名 `betterSidebar` 必须保留**。 **③ 造一个"改名归一化基座",让增量与改名解耦** ```bash git checkout -B tmp/norm-<旧基线> <旧基线 tag> # → 套同一口径改名 → commit git diff tmp/norm-<旧基线> HEAD -- . ':(exclude)pnpm-lock.yaml' > /tmp/apes-delta.patch # 校验:这条 diff 的文件清单必须恰好等于 ① 的「真改动 + 新增」 ``` **④ 在新基线上重建 + 三点合并** ```bash git checkout -B sync/<新基线 tag> <新基线 tag> # → 套同一口径改名 → commit("rename only") git apply -3 /tmp/apes-delta.patch # 冲突会以 <<<<<<< ours / >>>>>>> theirs 留存(命令本身不失败) ``` **⑤ 逐条判读冲突(只有五种形态)** | 形态 | 判定 | | --- | --- | | 版本/身份 JSON | 取**新基线**侧,再改成本 fork 的版本;两侧各自新增的元数据取**并集**(依赖、files 条目、`forkedFrom` 块) | | 上游搬走了某个 import | 取上游的新路径,保留本 fork 的新 import | | 上游新环境里的注册块 | 保留本 fork 的块,放进上游的新上下文 | | 同一函数两边都定义(上游上移/下移了它) | 删除本 fork 那份;用 `grep -c 'const '` 断言只剩 1 处 | | 长文档(README) | 整体取上游原文,再机械套上本 fork 的两处增量:顶部 fork notice + 包名替换(**跳过** `github.com/<上游 org>` 与徽章域名行) | **⑥ 迁移检查(API 漂移)+ 门禁** - 先**解完所有冲突标记再** `pnpm install`(`prepare: tsdown` 会因为语法错误直接失败), 并**删掉 lockfile 重新生成**(不要把旧 lockfile 带过来)。 - 逐条跑 `pnpm typecheck` 修类型漂移。经验:漂移通常集中在**本 fork 自己新增的模块**里。 上游会在 `context-types.ts` 的注释里写清迁移路径(例:0.1.6 的 `subagentsByParent` → 0.1.7 的 `projectionsBySession[id].values.subagentCatalog`,`list.current` 被删除),照注释做即可。 - 最后跑门禁三连,并且**用 `grep` 核对关键标记**(tab id、`srcDoc`、`dsh-desktop-content-viewport` 等)。 **⑦ 收尾(纪律)** - 安全锚点:`backup/pre-sync-<新基线>` 分支 + `apes-v<版本>` tag,**推送远端**。 - **不要把新线同步进正在使用的 profile**:peer 范围可能已跨 minor,同步过去只会「静默失效」, 同时把本机环境弄脏。保持 profile 在已验证的线上。 ## 七、提交与分支纪律 - 提交信息用 `type(scope): 摘要`(`feat` / `fix` / `chore` / `docs` / `refactor`),正文说明**为什么**与验证方式。 - 一个提交一件事;re-base 类改动拆成「rename only」+「增量移植」两个提交,便于回溯与回滚。 - 分支命名:`sync/<上游 tag>`(跟随上游)、`backup/pre-sync-<基线>`(锚点)、其余按需。 - 提交前:门禁三连 + 关键标记 grep + (若涉及部署)提醒 profile 重装与完整重启。