From 3fd6fef2585a1f95c5d8d1f9d9ccc9e72bb62bce Mon Sep 17 00:00:00 2001 From: Mucheen <1528136628@qq.com> Date: Sun, 25 Oct 2026 00:45:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugins):=20M0=20fixture=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E6=A3=80=E6=B5=8B=E4=B8=8E=20Windows=20=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=8C=85=E9=AA=8C=E6=94=B6=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/check-plugin-fixture-coverage.mjs:fixtures 目录真实枚举与显式 分类清单的覆盖检测器,新增未知/失效/重复/乱序/非法条目/证据路径失效 均明确失败,只读本地运行 - scripts/plugin-fixture-coverage.json:34 个 fixture 全量分类,3 wired / 10 validated-only / 10 indirect-core / 11 unwired,31 个 known gap 如实 归属路线图里程碑 - scripts/check-plugin-fixture-coverage.test.mjs:14 个单测覆盖全部失败 模式(临时目录正反例,有界清理),并强制真实仓库当前状态通过 - docs/development/windows-plugin-fixture-coverage.md:M0 审计快照与横切 发现(过时门控注释、契约 lock 落后、macOS 本地副本) - docs/development/windows-plugin-acceptance.md:M10 通用验收链、12 行 真实包矩阵与两阶段出口;全部 windows 工件/arm64 显式 pending --- docs/development/windows-plugin-acceptance.md | 110 ++++ .../windows-plugin-fixture-coverage.md | 119 ++++ scripts/check-plugin-fixture-coverage.mjs | 303 ++++++++++ .../check-plugin-fixture-coverage.test.mjs | 259 +++++++++ scripts/plugin-fixture-coverage.json | 521 ++++++++++++++++++ 5 files changed, 1312 insertions(+) create mode 100644 docs/development/windows-plugin-acceptance.md create mode 100644 docs/development/windows-plugin-fixture-coverage.md create mode 100644 scripts/check-plugin-fixture-coverage.mjs create mode 100644 scripts/check-plugin-fixture-coverage.test.mjs create mode 100644 scripts/plugin-fixture-coverage.json diff --git a/docs/development/windows-plugin-acceptance.md b/docs/development/windows-plugin-acceptance.md new file mode 100644 index 000000000..a86b50e51 --- /dev/null +++ b/docs/development/windows-plugin-acceptance.md @@ -0,0 +1,110 @@ +# Windows 插件真实包验收矩阵(M10 准备) + +状态:验收准备资料,不是验收记录。本文按 +[Windows 插件功能对齐计划](windows-plugin-parity-plan.md) M10 的要求,固定真实 Windows 插件包的身份、来源与逐项可复现验收步骤,并区分第一阶段(M1–M6)与第二阶段(M7–M9)出口。**本轮没有执行任何一次完整验收,也没有任何一行可以据此宣告 M10 完成。** + +数据核实:2026-10-24,gh API(仓库元数据、release 资产、`Lithe-IDEA/Lithe-store/lithe-plugin-index.json`、`Lithe-IDEA/lithe-plugin-docs`)+ 主仓库基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a` 的源码。索引与 release 会持续变化,每次执行验收前必须重新核实并把当时的 tag/版本/SHA-256 写进当次记录,不得沿用本文。 + +## 1. 通用验收流程(每个真实包逐项执行) + +以下链条来自路线图 M10 与 Windows 声明式插件宿主 Note 的既有边界,每个包按适用子流程裁剪,但裁剪必须在记录里写明。 + +| # | 步骤 | 通过标准 | 记录要求 | +| --- | --- | --- | --- | +| 1 | 前置固定 | 宿主构建 SHA、包 tag/commit、版本、下载 SHA-256、架构、构建配置(Release)全部登记 | 命令与输出 | +| 2 | 安装 | 市场下载按索引 SHA-256 核对(`plugin.archive.checksumMismatch` 拒绝篡改);磁盘导入走私有快照;staging→发布事务完成 | 安装来源字段与内容摘要 | +| 3 | 授权 | installReview 全量审批:权限 + 完整程序声明逐项可展开;社区包明示"未认证发布者";新增程序(newPrograms)高亮 | 审批界面截图 + 审批记录持久化确认 | +| 4 | 启用与重启 | 启用后重启应用,启动扫描结果一致;停用状态持久化;bundled 包仍为最高版本 | 扫描前后 catalog 对比 | +| 5 | 真实工作流 | 打开目标项目 → 语言服务启动/构建模型 → 运行、测试 → 取消一次在途操作;操作在真实 UI 触发,非仅命令调用 | 每步入口(菜单/按钮)、预期/实际 | +| 6 | 升级失败 | 更新时篡改摘要或中断下载:旧版本与旧安装记录保持可用,本次 staging 被清理,无"新目录配旧记录" | 失败注入方式与恢复证据 | +| 7 | 回滚 | 回滚到上一版本:先停止读用旧包的会话与页面,再换目录;回滚包按自身审批记录复验 | 页面关闭与进程停止证据 | +| 8 | 卸载 | 包目录、安装记录、所属页面、相关进程全部消失;无半删目录 | 卸载后目录与进程列表 | +| 9 | 进程树清理 | 每步结束后按包入口路径核对进程列表:语言服务器、Provider、构建子进程、辅助探测进程为零残留(Job Object 语义,非仅杀父 PID) | 进程快照(启动前/每步后) | +| 10 | 发行资源哈希不变 | 安装目录(含 `resource_dir()/OfficialPlugins` 等 bundled 资源)在验收前后的文件清单与哈希完全一致;运行时状态只出现在平台可写目录 | 前后哈希清单 diff | +| 11 | 证据归档 | 按路线图 §5.4:版本三元组、入口、复现步骤、预期/实际、测试命令与单测计时、哈希清单、进程清理、已知范围;截图先脱敏 | 存放位置与索引 | + +架构要求:`windows-x64` 与 `windows-arm64` 分别记录。没有 arm64 实机或可验证执行环境时保留 pending——**编译成功或 x64 仿真不替代 arm64 原生运行证据**。构建脚本已有 `aarch64-pc-windows-msvc` 目标标识(`scripts/build-windows.ps1`),但目前没有已发布的 arm64 工件与实机记录。 + +## 2. 真实包矩阵 + +"当前可得"列描述在本轮核实时点的可安装性。全部社区包当前只有 `darwin-*` 工件;**没有任何 windows-\* 工件存在**,这是第二列之外最大的统一 pending。 + +| 包 | 来源与版本(核实于 2026-10-24) | 许可 | Windows 入口(x64/arm64) | 外部 runtime | 当前可得 / 缺失 | 负责阶段 | +| --- | --- | --- | --- | --- | --- | --- | +| PhpSupportContract(bundled 契约包) | 主仓库 `Plugins/win/Official/PhpSupportContract`,版本 1.0.0,api2,`sameTeamAsHost`(基线 1e78b59b) | 仓库内 Lithe 资源;`intelephense.js` 为 65 字节占位入口 | runtime launch:node 启动 `language-server/bin/intelephense.js`;无 `${arch}` 路径,两架构同入口 | Node.js(宿主发现);`php` CLI(运行/测试 executable) | 已随宿主安装链验收(validate/launch/runPlan/testPlan/resolveSettings 端到端测试 `tests.rs:433`);真实 Intelephense 替换 pending——其许可禁止再分发(见 `2026-10-20-go-php-community-plugins` Note),Windows 分发方式需另行决定 | M10 第一阶段(完整实机链) | +| io.github.lithe-idea.php-support | [lithe-plugin-php](https://github.com/Lithe-IDEA/lithe-plugin-php) tag `v0.1.0`,api1 | Apache-2.0(仓库 license) | systemCommand `intelephense --stdio`;工具架构随用户安装 | Node.js(runtimeDependencies)+ 用户自装 intelephense(npm,许可禁止打包) | macOS 可装;**windows 工件 pending**;官方索引/商店均未列 windows 平台 | M2(市场安装链)→ M10 第一阶段 | +| io.github.lithe-idea.go-support | [lithe-plugin-go](https://github.com/Lithe-IDEA/lithe-plugin-go) tag `v0.1.0`,api1 | Apache-2.0 | systemCommand `gopls`;go 工具链架构随系统 | Go 工具链(runtimeDependencies,宿主发现) | macOS 可装;**windows 工件 pending** | M2 → M10 第一阶段 | +| io.github.lithe-idea.lua-support | [lithe-plugin-lua](https://github.com/Lithe-IDEA/lithe-plugin-lua) tag `v0.3.0`(另有 0.1.0/0.2.0),api1 | Apache-2.0 | packaged `bin/lithe-lua-language-server`——**架构相关二进制**,Windows 需按 x64/arm64 分别出工件 | 无(服务器自带) | macOS 可装(0.3.0 起分 arm64/x64 资产);**windows 工件 pending** | M2 → M10 第一阶段 | +| io.github.lithe-idea.gradle-build | [lithe-plugin-gradle](https://github.com/Lithe-IDEA/lithe-plugin-gradle) tag `v0.1.1`,api3,`requires {buildSystems:1}`,模型源 command | Apache-2.0 | 声明式构建工具窗口 + 命令计划;入口依赖构建工具(Gradle wrapper/gradle) | Gradle(项目 wrapper 或 PATH) | macOS 可装;Windows 当前 `apiUnsupported`(宿主未声明 buildSystems);**windows 工件 pending** | M4 开放能力 → M10 第一阶段 | +| io.github.lithe-idea.maven-build | [lithe-plugin-maven](https://github.com/Lithe-IDEA/lithe-plugin-maven) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:1, serverProcess:1}`,模型源 provider | Apache-2.0 | Provider 进程(自有 launch)+ Core litheBuild 协商 | JDK(provider 进程与 Maven 工具链) | macOS 当前不可装(未声明 buildProviders);Windows `apiUnsupported`;**windows 工件 pending** | M5 → M10 第一阶段(Provider/函数线) | +| io.github.lithe-idea.cargo-build | [lithe-plugin-cargo](https://github.com/Lithe-IDEA/lithe-plugin-cargo) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:2, serverProcess:1, settings:2}`,模型源 provider;0.1.0 为 `{buildSystems:1, buildProviders:1}` | Apache-2.0 | Provider 进程;cargo 工具链 | Rust/Cargo 工具链 | 0.1.1 声明 `buildSystems:2`——当前契约版本只定义到 1,需 Core 与宿主先行支持;0.1.0 双端均未开放对应能力;**windows 工件 pending** | M4/M5 → M10 第一阶段;buildSystems:2 属后续契约演进,不在冻结范围 | +| io.github.lithe-idea.svn-support | [lithe-plugin-svn](https://github.com/Lithe-IDEA/lithe-plugin-svn) tag `v0.1.0`,api3,`requires {vcsProviders:1, serverProcess:1}`,vcsTool svn | Apache-2.0 | VCS Provider 进程会话 | svn 命令行客户端(用户提供) | 双端当前均不可装(macOS 宿主也未声明 vcsProviders);**windows 工件 pending** | M9 → M10 第二阶段 | +| Java Support(dev.lithe.plugin.java-support) | macOS bundled 官方包;JDT LS 上游固定 [v1.61.0](https://github.com/eclipse-jdtls/eclipse.jdt.ls/tree/v1.61.0);包内含 `LICENSE-EPL-2.0.txt`(组装脚本 `assemble-package.sh` 随包携带) | JDT LS 及 Eclipse 扩展为 EPL-2.0(随包 LICENSE 文件);icons 目录 Apache-2.0 | Windows 无分发:路线图 M8 明确必须满足其整份 `requires`,不得复用 macOS ZIP(含 macOS 架构 JDK/runtime) | 打包 JDK(packagedRuntime,`${arch}` 路径) | macOS bundled 可用;**Windows 分发 pending(构建/打包决策未做)** | M8(生成器/分发)+ M10 第一阶段(语言链验收) | +| 官方可下载插件渠道 | `Plugins/marketplace/index.json` 与 `official-plugins.json` 均为空;发布管线 `release-official-plugins.yml` 存在(macos-26 runner 构建、tag 系列 ``) | 按未来包而定 | 官方索引当前无条目,无可验收的官方 Windows 包 | — | 渠道就绪但**无条目 → pending**;第一个官方包发布后按 §1 全链验收 | M2(索引/下载)→ M10 第一阶段 | +| 原生 UI 真实示例(ui:3) | 未选定外部示例仓库与版本 | 待选型 | native schema 树/列表/表单渲染 | 待选型 | **pending 选型**;路线图要求"固定版本的外部示例" | M7 → M10 第二阶段 | +| 项目生成器真实包(projectGenerators) | 未选定(`project-generator` fixture 仅覆盖 Core 语义;JavaSupport 模板为 macOS 分发) | 待选型 | 新建项目窗口 + files/command/archive 步骤 | 待选型 | **pending 选型** | M8 → M10 第二阶段 | + +补充说明: + +- 商店索引当前 7 个插件、12 个版本全部只列 `darwin-arm64`/`darwin-x64` 下载(gh 核实 `lithe-plugin-index.json`)。Windows 可用性在这些包发布 windows-\* 工件并更新索引之前,一律按 `plugin.index.platformUnsupported` 展示——这是契约定义的诚实状态,不能通过放宽校验绕过。 +- `cargo-build 0.1.1` 的 `buildSystems:2` 是矩阵里唯一超出当前契约定义版本的字段;记录于此仅作事实核对,不构成本轮工作项。 + +## 3. 分包类验收步骤(§1 通用链的裁剪点) + +### 3.1 语言插件(php-support / go-support / lua-support / Java Support) + +- 前置:目标 runtime 可被发现(Node/Go/JDK)或用户自装工具存在(intelephense/gopls);缺失时 LSP 状态显示缺少工具而不是崩溃。 +- 安装 → 审批(重点核对 systemCommand 程序声明的 `commands/arguments/validationArguments`)→ 启用 → 打开匹配扩展名的文件 → 语言服务器启动、补全/诊断可用 → 停用后进程退出、入口撤销 → 更新/回滚/卸载全链。 +- lua 的 packaged 二进制:额外核对拒绝包内 reparse point/路径逃逸、`${stateDir}` 状态目录不写包内。 +- Windows 特有:`LITHE_NODE_PATH`/`LITHE_GO_PATH` 导出与 PATH 前置在从开始菜单启动(非 shell)时生效。 + +### 3.2 构建插件(gradle-build / maven-build / cargo-build) + +- 前置:宿主能力声明已含 `buildSystems`/`buildProviders`(M4/M5 之后),市场侧展示可安装而不是不支持。 +- 检测(marker/nesting/priority)→ 工具窗口按声明注册 → 模型/依赖加载(command 或 provider)→ 选项修改与 `resolveOptions` → 任务/运行/测试执行与取消 → sync 文件变更触发过期提示。 +- maven/cargo 的 provider 模型:核对 litheBuild 协商失败可诊断、崩溃重启、`refresh` 语义;两个项目窗口互不串会话。 +- Windows 特有:Maven 原有专用流程不得退化;Worker/声明式的拥有者优先规则按计划 M0 审计结论执行。 + +### 3.3 Provider 函数与设置页(随 maven-build/cargo-build) + +- 网页 `functions.list/invoke/cancel` 走本插件 Provider;`settingActions` 按钮与网页同链路;`restart/immediate/reload/nextRun` 四种 effect 在真实进程上观察。 +- 关闭页面/项目、停用、更新时无残留调用与旧进度。 + +### 3.4 原生 UI 示例(M7 后) + +- 树/列表/表单/工具栏/右键动作渲染;中文输入、键盘导航、浅深主题、窄面板回退、状态恢复、并发动作;停用后入口与窗口撤销。 +- 更新 `declarative-native-plugin-ui` 矩阵前,上述全部为验收项而非演示项。 + +### 3.5 项目生成器(M8 后) + +- 欢迎页/菜单进入通用新建项目窗口 → 插件生成器字段与步骤 → 目标旁临时 staging → 成功后移动,失败/取消删除 → Git 初始化与 JDK 设置 → 打开后进入语言导入与构建识别。 +- 目标目录已存在或被并发创建时拒绝覆盖;失败无半成品;成功不写安装目录。 + +### 3.6 VCS Provider(M9 后) + +- 根识别 → 状态/提交/更新/回滚入口按协商能力显示 → 关闭项目窗口停止 Provider 会话 → 嵌套根归属最深 → 缺少 svn 客户端、凭据/网络失败按稳定码展示。 +- Git 主流程不回归是并行退出条件。 + +## 4. 阶段出口核对单 + +第一阶段出口(M1–M6 完成后执行): + +- [ ] 至少一个真实市场包完成 §1 全链(1–11)并归档证据。 +- [ ] 语言(php/go/lua 之一)、构建(gradle/maven 之一)、Provider+函数与设置页各有真实包行完成。 +- [ ] Windows bundled PhpSupportContract 用真实 Intelephense(或明确的分发决定)完成完整实机链。 +- [ ] windows-x64 每行有记录;windows-arm64 的 pending 逐行显式列出。 +- [ ] 每行发行资源哈希不变 + 进程树零残留核对通过。 +- [ ] `plugin-functions`、`plugin-registered-settings-pages`、网页桥相关矩阵按实际范围更新(归属相应里程碑 PR,不在本文)。 + +第二阶段出口(M7–M9 完成后执行): + +- [ ] 原生 UI 示例、项目生成器包、VCS Provider 包各自完成适用子流程。 +- [ ] 生成器"生成 → 打开 → 语言服务 → 构建/运行"链有真实包证据。 +- [ ] arm64 pending 重新评估;仍缺实机则继续显式 pending。 +- [ ] 哈希/进程清理要求同第一阶段。 + +## 5. 数据来源与更新规则 + +- 仓库元数据/release/索引:gh API(`repos/Lithe-IDEA/*`、`repos/Lithe-IDEA/Lithe-store/contents/lithe-plugin-index.json`);许可取仓库 license 字段与包内 LICENSE 文件。 +- 契约语义与宿主边界:`shared/contracts/plugins/v1/README.md`、[Windows 声明式插件宿主 Note](../../.agents/notes/implemented/architecture/2026-10-22-windows-declarative-plugin-host.md)、[Go/PHP 社区化 Note](../../.agents/notes/implemented/architecture/2026-10-20-go-php-community-plugins.md)、[Java 语言服务器插件 Note](../../.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md)。 +- 更新规则:本文只固定"验收输入的身份与步骤"。每次执行验收时重新 fetch 来源,把当时的 tag、版本、SHA-256、架构、宿主 SHA 写入当次验收记录;本文矩阵行与当次记录冲突时,以当次记录为准并回写本文。不得在本文新增下载物、私钥或无版本生成资源;不把模拟测试或单测当作实机交付。 diff --git a/docs/development/windows-plugin-fixture-coverage.md b/docs/development/windows-plugin-fixture-coverage.md new file mode 100644 index 000000000..68b037a56 --- /dev/null +++ b/docs/development/windows-plugin-fixture-coverage.md @@ -0,0 +1,119 @@ +# Windows 插件 fixture 覆盖审计(M0) + +状态:M0 交付。证据在基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a`(#1424 合入后的 preview 起点)上逐一核对,行号为该基线时点。 + +本文回答路线图 +[Windows 插件功能对齐计划](windows-plugin-parity-plan.md) M0 的要求:`shared/contracts/plugins/v1/fixtures` 的每个文件,是否有 Core 契约验证、是否有 Windows 产品宿主的**真实接线**测试,缺什么、归哪个阶段。机器可读清单是 +`scripts/plugin-fixture-coverage.json`,由 +`scripts/check-plugin-fixture-coverage.mjs` 持续校验;本文是人类可读的审计快照,两者由检测器的强制单测绑定。 + +**总原则:Core schema 校验通过不等于 Windows 产品完成。** `invalid-manifests.json` 的 154 个用例循环只证明"宿主校验路径对同一批非法输入报同一个稳定错误码",不证明安装、启动、停用、卸载、会话回收等宿主行为已经接线。下文按此口径标注。 + +## 1. 结论摘要 + +- fixtures 目录共 34 个文件:19 个合法清单(含原生 UI 3 个、项目生成 1 个、VCS 1 个,分类时单列)、1 个非法修改案例集、5 个金样/计划文件(command-plans、build-plans、runtime-versions、function-protocol、project-plans)、5 个市场用例、4 个项目生成模板。另有 `README.md` 与两个 schema 在契约根目录,不在 fixtures 内。 +- Core 侧:34 个文件全部有至少一个 Core 测试引用(模板目录经 `plan.rs` 的 `collect_files` 生产代码递归读取,被 golden 测试间接触发)。 +- Windows 侧:**3 个 wired**(有产品路径测试)、**10 个 validated-only**(仅清单验收/能力门控)、**10 个 indirect-core**(Windows 经 Core 命令消费、无直接读取)、**11 个 unwired**(Windows 零引用)。 +- 最严重的真实缺口:`build-tool-path`、`build-provider-language-server`、`native-panel`、`native-form`、`native-controls`、`project-generator`、`vcs-provider` 在 Windows 侧零引用;全部 5 个市场用例和 5 个跨平台金样在 Windows 没有直接消费测试。 +- 顺带发现(只记录,不修改,归属见 §5):`tests.rs:149` 的门控注释已过时;`lithe-plugin-docs` 的 `contract.lock.json` 落后于主仓库契约;macOS `BuildSystemToolWindowTests` 用本地 fixture 副本,共享 fixture 的改动不会反映到它。 + +## 2. 检测工具与运行方式 + +```bash +node scripts/check-plugin-fixture-coverage.mjs # 清单 vs 目录真实枚举 +node --test scripts/check-plugin-fixture-coverage.test.mjs +``` + +检测器只做本地只读比较,失败即退出码 1,并逐行打印问题。它强制: + +| 失败类型 | 含义 | +| --- | --- | +| `unknown-fixture` | 目录里出现清单未分类的新文件(新增 fixture 忘了登记) | +| `missing-fixture` | 清单条目指向的文件已不存在(失效路径) | +| `duplicate-entry` | 同一文件被分类两次 | +| `unsorted-entry` | 清单未按路径排序(保持 diff 可审) | +| `invalid-entry` | 枚举值非法、wired 无 Windows 证据、known gap 缺阶段/说明、路径逃逸等 | +| `evidence-path` | 证据引用的仓库文件不存在(测试文件被移动/删除后证据过期) | +| `io` | 清单/目录不可读 | + +单测覆盖正反例(临时目录构造,全部同步、有界、`t.after` 清理),最后一条强制真实仓库当前状态通过,并断言 known gap 数量大于零——**不允许把状态全部改成 wired 来伪造覆盖**。关闭一个 gap 的唯一方式是给出真实的 Windows 测试引用。 + +清单里每个条目的证据字符串格式是 `<仓库相对路径>[:<行号>] <测试名>`。检测器只校验文件存在;行号是基线时点的文档,允许随后续重构漂移,但文件消失必须处理。 + +## 3. 覆盖状态语义 + +| 字段 | 值 | 含义 | +| --- | --- | --- | +| `outcome` | success / rejection / partial-support | 契约角色:成功路径 / 拒绝路径 / 合法但当前双端宿主均未完整开放的能力(build provider 两形态) | +| `windowsStatus` | wired | Windows 有产品路径测试直接使用该 fixture(扫描、安装、启动、catalog、Web 窗口、前端注册等,不止清单校验) | +| | validated-only | 仅进入 Windows 清单验收(带/不带能力门控),无产品路径测试 | +| | indirect-core | Windows 通过 Core 命令消费其内容,宿主测试不直接读取该文件 | +| | unwired | Windows 零直接引用(或仅作为 invalid-manifests 基底间接读取,单独说明) | + +非 wired 的每条都带 `gapStage`(路线图里程碑 M0–M10)与 `gapDetail`。`gapStage` 表示**计划中该能力接入的阶段**,不是承诺日期;提前实现时以真实测试关闭并更新清单。 + +## 4. 分类明细 + +### 4.1 合法清单(14 个 valid-manifest) + +| Fixture | 结果 | Core 入口(节选) | Windows 入口 | Windows 状态 / 缺口 | +| --- | --- | --- | --- | --- | +| language-packaged-server | success | `declarative_plugins.rs:65` 全量验证;`:81` 归一化;`:251` API1 参数字面量;`marketplace_plugins.rs:219` 审查金样 | `tests.rs:112/162/196/258` 验收+扫描+存储+catalog;`install_tests.rs:43/118/185` 快照/staging/回滚;`launch.rs:933` 启动计划解析 | **wired** | +| hello-lithe | success | `declarative_plugins.rs:65/532/610` UI 贡献与图标归一化 | `tests.rs:112`;`web.rs:1657` 文本窗口注册;前端 `declarative-plugin-ui.test.ts:33` 三处命令位与撤销 | **wired** | +| language-system-command | success | `declarative_plugins.rs:65/106/330` 隐藏文件名、probe 不得用设置 | `tests.rs:112` 验收;`tests.rs:258` bundled catalog | validated-only → M10:systemCommand 启动(PATH 解析、`LITHE_*_PATH` 导出)无 fixture 级测试;macOS 侧也零引用 | +| language-host-integration | success | `declarative_plugins.rs:65/152/276` 集成能力归一化与审查 | `tests.rs:112` 仅验收 | validated-only → M10:jdt 集成启动计划与必需包资源无宿主测试(`launch.rs` 测试用 packaged-server),真实 JDT 包随 M10 | +| node-runtime | success | `declarative_plugins.rs:65/276/351` runtime 启动审查与设置归一化 | `tests.rs:112` 仅验收 | validated-only → M10:packagedRuntime/minimumVersion 与设置解析无 fixture 级测试(PHP 契约包端到端覆盖了 runtime node 计划展开,但不是本清单) | +| web-panel | success | `declarative_plugins.rs:65/563` 网页窗口许可与包页面 | `tests.rs:112` 仅验收(宿主已声明 `ui:[1,2]`) | validated-only → M6:本 fixture 声明的包页面没有被任何宿主测试打开;`build.*`/`functions.*` 桥按 v0 边界拒绝 | +| settings-v2 | success | `declarative_plugins.rs:901/911/958` 即时设置、分层解析、危险使用拒绝 | `tests.rs:112` 验收 + `tests.rs:128` 门控断言 | validated-only → M4:注册设置页、settingActions 与分层覆盖缺 fixture 级宿主行为测试(现有覆盖走设置编辑器与 `settings_tests`,不经本清单)。门控原因是该 fixture 同时 `requires buildSystems/buildProviders`;`tests.rs:149` 注释表述过时,见 §5 | +| build-static-file | success | `build_systems.rs:128/143/218/361/1236` 检测、golden、运行项、symlink 拒绝 | `tests.rs:112` + `:128` 门控 | validated-only → M4 | +| build-command-stdout | success | `build_systems.rs:128/143/1071` | `tests.rs:112` + `:128` | validated-only → M4 | +| build-command-output-file | success | `build_systems.rs:128/143` | `tests.rs:112` + `:128` | validated-only → M4 | +| build-host-integration | success | `build_systems.rs:128/143/482`;`build_runtime_versions.rs:15`;`project_generators.rs:92` | `tests.rs:112` + `:128` | validated-only → M4 | +| build-provider | partial-support | `declarative_plugins.rs:445`;`build_systems.rs:1115`;`marketplace_plugins.rs:219/332` | `tests.rs:112` + `:128` 门控 | validated-only → M5:自有进程 Provider 启动、litheBuild 协商、会话回收无宿主测试 | +| build-provider-language-server | partial-support | `declarative_plugins.rs:366/445`;`build_systems.rs:1460`;`marketplace_plugins.rs:219` | 仅经 `tests.rs:80` 作为 4 个非法用例基底间接读取 | **unwired** → M5:不在任何 Windows 显式清单,连"被门控拒绝"都无专属断言 | +| build-tool-path | success | `build_systems.rs:1599/1666/1719/1755` executableResolution 全部语义 | 无 | **unwired** → M4:Windows 零引用 | + +### 4.2 非法修改案例、跨平台金样与函数协议(6 个) + +| Fixture | 结果 | Core 入口 | Windows 入口 | Windows 状态 / 缺口 | +| --- | --- | --- | --- | --- | +| invalid-manifests.json(154 用例,12 个基底) | rejection | `declarative_plugins.rs:130` | `tests.rs:80` 全量循环,宿主校验路径断言同码 | **wired**(校验路径一致性;宿主行为仍以各清单的 wired/validated-only 行为准) | +| command-plans.json | success | `declarative_plugins.rs:212` golden 展开 | 无;`tests.rs:433` 的 PHP 契约包端到端实测了 runPlan/testPlan 展开但不用本 golden | indirect-core → M5 | +| build-plans.json | success | `build_systems.rs:143` | 无 | indirect-core → M4 | +| runtime-versions.json | success | `declarative_plugins.rs:312` | 无(`launch.rs` 用 node/java/go 真实输出自测解析,不读本 golden) | indirect-core → M10:建议接入宿主探测路径时用本 fixture 对齐 | +| function-protocol.json | success | `plugin_function.rs:441` 起 7 组协议测试 | 无;网页桥 v0 对 `functions.*` 返回 `plugin.web.unknownMethod` | indirect-core → M6 | +| project-plans.json | success | `project_generators.rs:65` | 无 | indirect-core → M8 | + +### 4.3 市场用例(5 个 marketplace/) + +全部仅 Core:`marketplace_plugins.rs:100`(索引可用性)、`:131`(归一化排序)、`:147`(非法条目拒绝)、`:203`(来源 URL 解析)、`:219`(安装审查金样)、`:272`(ui 可用性需平台宿主)、`:332`(Provider 版本需宿主支持)。Windows 全部 indirect-core → M2:声明式市场宿主未接,索引消费、下载校验与来源记录均无 Windows 测试。其中 `install-reviews.json` 补充:Windows 磁盘安装链已调用 `installReview` 命令,但测试用 `language-packaged-server` 清单而非本审查金样矩阵。 + +### 4.4 原生 UI、项目生成与 VCS(9 个) + +| Fixture | Core 入口 | Windows 入口 | Windows 状态 / 缺口 | +| --- | --- | --- | --- | +| native-panel.plugin.json | `declarative_plugins.rs:666/785` 版本归属、动作与预算 | 无 | **unwired** → M7 | +| native-form.plugin.json | `declarative_plugins.rs:721/785` 表单值/动作/网格 | 无 | **unwired** → M7 | +| native-controls.plugin.json | `declarative_plugins.rs:827` 公共控件类型/范围/按钮 | 无 | **unwired** → M7 | +| project-generator.plugin.json | `project_generators.rs:65/92/259/327/339` 表单/计划/归档/审查 | 无 | **unwired** → M8 | +| project-generator-templates/.gitattributes | 经 `plan.rs collect_files` 递归读取(字节稳定标记) | 无 | indirect-core → M8 | +| project-generator-templates/app/README.md | 同上(files 步骤模板) | 无 | indirect-core → M8 | +| project-generator-templates/app/dot-gitignore | 同上(dot 命名规避 git 忽略) | 无 | indirect-core → M8 | +| project-generator-templates/main/Main.java | 同上(条件占位符模板) | 无 | indirect-core → M8 | +| vcs-provider.plugin.json | `vcs.rs:65/94/147/196` 审查、权限、启动计划、根识别 | 无 | **unwired** → M9 | + +## 5. 横切发现(记录,不在本 PR 修改) + +1. **过时门控注释**:`windows/tauri/src-tauri/src/plugin_host/tests.rs:149` 注释称 settings-v2 被门控是因为"settings 2 与 languageServers 2 未声明";实际 `mod.rs::host_capabilities` 已声明 `settings:[1,2]`、`languageServers:[1,2]`,真正的门控原因是该 fixture 同时 `requires buildSystems:1, buildProviders:1`。断言本身仍通过且有效,注释需在下次触碰该测试的 PR 中修正。 +2. **契约同步缺口**:`Lithe-IDEA/lithe-plugin-docs` 的 `contract.lock.json`(gh 核实于 2026-10-24)固定在旧修订 `18345cc9…`,文件清单缺少后加入的 `native-panel/native-form/native-controls`、`project-generator`、`project-generator-templates/` 全目录与 `project-plans.json`。契约 README 规定契约改动必须同步该仓库;这属于 [插件契约发布](../../.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md) 所有者的待办。 +3. **macOS 本地副本**:`macos/Tests/LitheTests/Fixtures/BuildSystems/` 下的 `build-command-output-file`、`build-host-integration` 等是共享 fixture 的本地副本,`BuildSystemToolWindowTests` 经 `Bundle.module` 加载副本。共享 fixture 语义变更不会自动反映到这批测试,属已知重复面,不在本轮收敛。 +4. **非法清单循环的边界**:Windows `invalid-manifests` 循环(154 用例)与 macOS 同名测试是契约一致性回归;它们不能替代宿主行为测试,也不能作为"Windows 已支持某能力"的证据——被门控清单(如 build 组)的全量拒绝恰恰说明能力未开放。 +5. **fixtur 目录枚举现状**:Core 测试全部用 `include_str!`/显式 const 清单,Windows 测试用 `read_fixture(name)` 显式点名;唯一对 fixtures 树的运行时递归读取是 `plan.rs` 的模板收集。这就是"新增 fixture 会静默漏掉"的结构性原因,也是本检测器存在的理由。`scripts/verify-shared-contracts.sh` 的语义校验(schemaVersion、ID 前缀、拒绝码文档化等)与本检测器互补,不互相替代。 + +## 6. 维护规则 + +- 新增 fixture:同 PR 在 `scripts/plugin-fixture-coverage.json` 登记分类与双端证据;Windows 尚无测试时如实标 known gap(状态、阶段、说明),不得标 wired。 +- 删除/重命名 fixture:同 PR 清理清单,否则 `missing-fixture` 失败。 +- 移动/删除被引用的测试文件:更新证据条目,否则 `evidence-path` 失败。 +- 关闭 gap:以真实 Windows 测试为证据更新 `windowsStatus`/`windowsEvidence` 并清空 gap 字段;检测器会拒绝"没有证据的 wired"。 +- 本文是审计快照;实时约束以清单 + 检测器为准。行号漂移不需要修本文,但证据文件消失必须处理。 diff --git a/scripts/check-plugin-fixture-coverage.mjs b/scripts/check-plugin-fixture-coverage.mjs new file mode 100644 index 000000000..941ed255b --- /dev/null +++ b/scripts/check-plugin-fixture-coverage.mjs @@ -0,0 +1,303 @@ +#!/usr/bin/env node +// Fixture coverage detector for shared/contracts/plugins/v1/fixtures. +// +// Every fixture file must appear exactly once in the explicit classification +// list scripts/plugin-fixture-coverage.json, and every list entry must point +// at a real file. This closes the "fixtures added without classification" +// hole: a new file in the directory fails with unknown-fixture, a stale list +// entry fails with missing-fixture, a file classified twice fails with +// duplicate-entry. The list must stay sorted so diffs stay reviewable, and a +// non-wired Windows status is a known gap that must name a roadmap stage +// (M0-M10) and a concrete detail until a real test closes it. +// +// Usage: +// node scripts/check-plugin-fixture-coverage.mjs +// [--manifest scripts/plugin-fixture-coverage.json] +// [--repo-root ] [--fixtures-dir ] +// +// Exits 0 when the enumeration and the list match and every entry is +// coherent; exits 1 after printing one line per problem. Read-only: it never +// writes anything and needs no network. Unit tests live in +// scripts/check-plugin-fixture-coverage.test.mjs. +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { parseArgs } from 'node:util'; + +export const CATEGORY_VALUES = [ + 'valid-manifest', + 'invalid-edits', + 'command-plans', + 'build-plans', + 'runtime-versions', + 'function-protocol', + 'project-plans', + 'project-generation', + 'marketplace', + 'native-ui', + 'vcs-provider', + 'template-assets', +]; + +export const OUTCOME_VALUES = ['success', 'rejection', 'partial-support']; + +export const WINDOWS_STATUS_VALUES = ['wired', 'validated-only', 'indirect-core', 'unwired']; + +const GAP_STAGE_PATTERN = /^M(10|[1-9])$/; + +export function defaultRepoRoot() { + return join(dirname(fileURLToPath(import.meta.url)), '..'); +} + +export function defaultManifestPath() { + return join(defaultRepoRoot(), 'scripts', 'plugin-fixture-coverage.json'); +} + +// Recursively lists regular files under fixturesDir as '/'-separated paths +// relative to it, in the same byte order the manifest must be sorted with. +// Any non-directory, non-regular entry (symlink, junction) is reported as a +// fixture candidate so an anomalous entry cannot hide behind the enumeration. +export function enumerateFixtureFiles(fixturesDir) { + if (!statSync(fixturesDir, { throwIfNoEntry: false })?.isDirectory()) { + throw new Error(`fixtures directory is not readable: ${fixturesDir}`); + } + const found = []; + const walk = (absolute, prefix) => { + for (const entry of readdirSync(absolute, { withFileTypes: true })) { + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + walk(join(absolute, entry.name), relativePath); + } else { + found.push(relativePath); + } + } + }; + walk(fixturesDir, ''); + found.sort(comparePaths); + return found; +} + +function comparePaths(left, right) { + if (left < right) return -1; + if (left > right) return 1; + return 0; +} + +// An evidence string is '[:] '. +// Only the file part is enforced (test files move); the line number is +// documentation of where the evidence was read at the audit's base commit. +export function evidenceFilePath(evidence) { + const token = evidence.trim().split(/\s+/)[0] ?? ''; + const match = token.match(/^(.+):(\d+)$/); + return match ? match[1] : token; +} + +// Checks the classification list against the real directory enumeration. +// Returns { ok, problems, summary }; problems are { kind, detail } and the +// summary counts fixtures, categories, wired entries, and known gaps. +export function runCoverageCheck({ repoRoot, fixturesDir, manifest }) { + const problems = []; + const push = (kind, detail) => problems.push({ kind, detail }); + + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + return { + ok: false, + problems: [{ kind: 'io', detail: 'manifest is not a JSON object' }], + summary: null, + }; + } + if (manifest.schemaVersion !== 1) { + push('io', `manifest schemaVersion must be 1, got ${JSON.stringify(manifest.schemaVersion)}`); + } + const fixtures = manifest.fixtures; + if (!Array.isArray(fixtures)) { + push('io', 'manifest fixtures must be an array'); + return { ok: false, problems, summary: null }; + } + + let onDisk; + try { + onDisk = enumerateFixtureFiles(fixturesDir); + } catch (error) { + push('io', String(error.message ?? error)); + return { ok: false, problems, summary: null }; + } + + const byPath = new Map(); + let previousPath = null; + for (const [index, entry] of fixtures.entries()) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + push('invalid-entry', `fixtures[${index}] is not an object`); + continue; + } + const path = entry.path; + if (typeof path !== 'string' || path.length === 0) { + push('invalid-entry', `fixtures[${index}].path must be a non-empty string`); + continue; + } + if (byPath.has(path)) { + push('duplicate-entry', `${path} is classified at indexes ${byPath.get(path)} and ${index}`); + continue; + } + byPath.set(path, index); + if (previousPath !== null && comparePaths(previousPath, path) >= 0) { + push('unsorted-entry', `${path} must come after ${previousPath}; keep the list sorted`); + } + previousPath = path; + validateEntry(entry, repoRoot, push); + } + + const listed = new Set(byPath.keys()); + for (const path of onDisk) { + if (!listed.has(path)) push('unknown-fixture', `${path} exists in the fixtures directory but is not classified`); + } + for (const path of listed) { + if (!onDisk.includes(path)) push('missing-fixture', `${path} is classified but does not exist in the fixtures directory`); + } + + const classified = [...byPath.keys()]; + const summary = { + fixtures: classified.length, + onDisk: onDisk.length, + byCategory: {}, + wired: 0, + knownGaps: 0, + gapStages: {}, + }; + for (const path of classified) { + const entry = fixtures[byPath.get(path)]; + if (!entry || typeof entry !== 'object') continue; + summary.byCategory[entry.category] = (summary.byCategory[entry.category] ?? 0) + 1; + if (entry.windowsStatus === 'wired') summary.wired += 1; + else { + summary.knownGaps += 1; + if (typeof entry.gapStage === 'string') { + summary.gapStages[entry.gapStage] = (summary.gapStages[entry.gapStage] ?? 0) + 1; + } + } + } + + return { ok: problems.length === 0, problems, summary }; +} + +function validateEntry(entry, repoRoot, push) { + const path = entry.path; + if (typeof path === 'string' && path.length > 0) { + if (path.includes('\\') || path.startsWith('/') || path.split('/').includes('..')) { + push('invalid-entry', `${path}: fixture paths must be '/'-separated and stay inside the fixtures directory`); + } + } + if (!CATEGORY_VALUES.includes(entry.category)) { + push('invalid-entry', `${path ?? ''}: unknown category ${JSON.stringify(entry.category)}`); + } + if (!OUTCOME_VALUES.includes(entry.outcome)) { + push('invalid-entry', `${path ?? ''}: unknown outcome ${JSON.stringify(entry.outcome)}`); + } + if (!WINDOWS_STATUS_VALUES.includes(entry.windowsStatus)) { + push('invalid-entry', `${path ?? ''}: unknown windowsStatus ${JSON.stringify(entry.windowsStatus)}`); + } + checkEvidence(entry.path, entry.coreEvidence, 'coreEvidence', repoRoot, push); + checkEvidence(entry.path, entry.windowsEvidence, 'windowsEvidence', repoRoot, push); + + const wired = entry.windowsStatus === 'wired'; + if (wired) { + if (!Array.isArray(entry.windowsEvidence) || entry.windowsEvidence.length === 0) { + push('invalid-entry', `${path}: windowsStatus "wired" requires at least one windowsEvidence entry`); + } + if (entry.gapStage !== null && entry.gapStage !== undefined) { + push('invalid-entry', `${path}: a wired fixture must not carry gapStage`); + } + if (entry.gapDetail !== null && entry.gapDetail !== undefined) { + push('invalid-entry', `${path}: a wired fixture must not carry gapDetail`); + } + } else if (WINDOWS_STATUS_VALUES.includes(entry.windowsStatus)) { + if (typeof entry.gapStage !== 'string' || !GAP_STAGE_PATTERN.test(entry.gapStage)) { + push('invalid-entry', `${path}: a known gap must name a roadmap stage (M0-M10), got ${JSON.stringify(entry.gapStage)}`); + } + if (typeof entry.gapDetail !== 'string' || entry.gapDetail.trim().length === 0) { + push('invalid-entry', `${path}: a known gap must describe what is missing in gapDetail`); + } + } +} + +function checkEvidence(path, evidence, field, repoRoot, push) { + if (evidence === undefined) { + if (field === 'coreEvidence') push('invalid-entry', `${path ?? ''}: coreEvidence is required`); + return; + } + if (!Array.isArray(evidence)) { + push('invalid-entry', `${path ?? ''}: ${field} must be an array of strings`); + return; + } + if (field === 'coreEvidence' && evidence.length === 0) { + push('invalid-entry', `${path}: coreEvidence must name at least one Core-side reference`); + return; + } + for (const item of evidence) { + if (typeof item !== 'string' || item.trim().length === 0) { + push('invalid-entry', `${path ?? ''}: ${field} entries must be non-empty strings`); + continue; + } + const file = evidenceFilePath(item); + if (file.length === 0) { + push('invalid-entry', `${path}: ${field} entry does not start with a file path: ${item}`); + continue; + } + if (!existsSync(join(repoRoot, ...file.split('/')))) { + push('evidence-path', `${path}: ${field} references a missing file: ${file}`); + } + } +} + +function printReport(result) { + const { problems, summary } = result; + for (const problem of problems) console.error(`${problem.kind}: ${problem.detail}`); + if (summary) { + const stages = Object.entries(summary.gapStages) + .sort(comparePaths) + .map(([stage, count]) => `${stage}:${count}`) + .join(' '); + console.log( + `fixtures=${summary.fixtures} onDisk=${summary.onDisk} wired=${summary.wired} knownGaps=${summary.knownGaps}` + + (stages ? ` gapStages(${stages})` : ''), + ); + } +} + +function main() { + const { values } = parseArgs({ + options: { + manifest: { type: 'string' }, + 'repo-root': { type: 'string' }, + 'fixtures-dir': { type: 'string' }, + }, + }); + const repoRoot = values['repo-root'] ?? defaultRepoRoot(); + const manifestPath = values.manifest ?? join(repoRoot, 'scripts', 'plugin-fixture-coverage.json'); + + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + } catch (error) { + console.error(`io: cannot read manifest ${manifestPath}: ${error.message}`); + process.exitCode = 1; + return; + } + + const fixturesDir = + values['fixtures-dir'] ?? + (typeof manifest.fixturesDirectory === 'string' ? join(repoRoot, ...manifest.fixturesDirectory.split('/')) : null); + if (!fixturesDir) { + console.error('io: manifest fixturesDirectory is missing and --fixtures-dir was not given'); + process.exitCode = 1; + return; + } + + const result = runCoverageCheck({ repoRoot, fixturesDir, manifest }); + printReport(result); + process.exitCode = result.ok ? 0 : 1; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/scripts/check-plugin-fixture-coverage.test.mjs b/scripts/check-plugin-fixture-coverage.test.mjs new file mode 100644 index 000000000..60b5e3d8c --- /dev/null +++ b/scripts/check-plugin-fixture-coverage.test.mjs @@ -0,0 +1,259 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + defaultManifestPath, + defaultRepoRoot, + enumerateFixtureFiles, + evidenceFilePath, + runCoverageCheck, +} from './check-plugin-fixture-coverage.mjs'; + +// A throwaway repository shaped like the real one: a fixtures directory, a +// classification manifest, and evidence files the manifest can point at. +// Every wait is synchronous filesystem work, so the tests are deterministic +// and clean their temp roots through t.after on success and failure alike. +function tempRepo(t) { + const root = mkdtempSync(join(tmpdir(), 'lithe-fixture-coverage-test-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const fixturesDirectory = 'shared/contracts/plugins/v1/fixtures'; + const fixturesDir = join(root, ...fixturesDirectory.split('/')); + return { + root, + fixturesDir, + fixturesDirectory, + writeFixture(relativePath) { + mkdirSync(dirname(join(fixturesDir, ...relativePath.split('/'))), { recursive: true }); + writeFileSync(join(fixturesDir, ...relativePath.split('/')), '{}\n'); + }, + writeManifest(entries, extra = {}) { + const manifestPath = join(root, 'scripts', 'plugin-fixture-coverage.json'); + mkdirSync(dirname(manifestPath), { recursive: true }); + writeFileSync( + manifestPath, + JSON.stringify({ schemaVersion: 1, fixturesDirectory, fixtures: entries, ...extra }, null, 2), + ); + }, + check() { + const manifest = JSON.parse(readFileSync(join(root, 'scripts', 'plugin-fixture-coverage.json'), 'utf8')); + return runCoverageCheck({ repoRoot: root, fixturesDir, manifest }); + }, + }; +} + +// Evidence strings are repo-relative, like the real manifest's. +const fixtureEvidence = (relativePath) => `shared/contracts/plugins/v1/fixtures/${relativePath}:1 exists`; + +function entry(path, overrides = {}) { + const evidence = [fixtureEvidence(path)]; + return { + path, + category: 'valid-manifest', + outcome: 'success', + coreEvidence: evidence, + windowsEvidence: evidence, + windowsStatus: 'wired', + gapStage: null, + gapDetail: null, + ...overrides, + }; +} + +const kinds = (result) => result.problems.map((problem) => problem.kind); + +test('a matching manifest and directory pass with an honest summary', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.plugin.json'); + repo.writeFixture('nested/b.json'); + repo.writeManifest([ + entry('a.plugin.json'), + entry('nested/b.json', { + category: 'marketplace', + windowsStatus: 'unwired', + gapStage: 'M2', + gapDetail: 'no Windows consumer yet', + windowsEvidence: [], + }), + ]); + const result = repo.check(); + assert.equal(result.ok, true); + assert.deepEqual(result.problems, []); + assert.equal(result.summary.fixtures, 2); + assert.equal(result.summary.wired, 1); + assert.equal(result.summary.knownGaps, 1); + assert.deepEqual(result.summary.gapStages, { M2: 1 }); + assert.deepEqual(result.summary.byCategory, { 'valid-manifest': 1, marketplace: 1 }); +}); + +test('a new file in the fixtures directory fails as unknown', (t) => { + const repo = tempRepo(t); + repo.writeFixture('classified.json'); + repo.writeFixture('surprise.plugin.json'); + repo.writeManifest([entry('classified.json')]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['unknown-fixture']); + assert.match(result.problems[0].detail, /surprise\.plugin\.json/); +}); + +test('a classified file that no longer exists fails as missing', (t) => { + const repo = tempRepo(t); + repo.writeFixture('present.json'); + repo.writeManifest([ + entry('present.json'), + // Its evidence points at a surviving file so only the missing fixture is reported. + entry('removed.json', { + coreEvidence: [fixtureEvidence('present.json')], + windowsEvidence: [fixtureEvidence('present.json')], + }), + ]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['missing-fixture']); + assert.match(result.problems[0].detail, /removed\.json/); +}); + +test('classifying the same fixture twice fails', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeManifest([entry('a.json'), entry('a.json')]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['duplicate-entry']); +}); + +test('an unsorted list fails so diffs stay reviewable', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeFixture('b.json'); + repo.writeManifest([entry('b.json'), entry('a.json')]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['unsorted-entry']); +}); + +test('incoherent entries fail: unknown enums, wired without evidence, gap without stage', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeFixture('b.json'); + repo.writeFixture('c.json'); + repo.writeFixture('d.json'); + repo.writeManifest([ + entry('a.json', { category: 'mystery', windowsStatus: 'indirect-core', gapStage: 'M4', gapDetail: 'x', windowsEvidence: [] }), + entry('b.json', { windowsEvidence: [] }), + entry('c.json', { windowsStatus: 'indirect-core', gapDetail: 'stage forgotten', windowsEvidence: [] }), + entry('d.json', { coreEvidence: [] }), + ]); + const result = repo.check(); + assert.equal(result.ok, false); + const details = result.problems.map((problem) => problem.detail); + assert.ok(details.some((detail) => detail.includes('unknown category "mystery"')), details.join('\n')); + assert.ok(details.some((detail) => detail.includes('b.json: windowsStatus "wired" requires')), details.join('\n')); + assert.ok(details.some((detail) => detail.includes('c.json: a known gap must name a roadmap stage')), details.join('\n')); + assert.ok(details.some((detail) => detail.includes('d.json: coreEvidence must name')), details.join('\n')); +}); + +test('a wired fixture must not carry gap fields and a gap must describe the miss', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeFixture('b.json'); + repo.writeManifest([ + entry('a.json', { gapStage: 'M4', gapDetail: 'stray' }), + entry('b.json', { windowsStatus: 'unwired', gapStage: 'M4', gapDetail: ' ' }), + ]); + const result = repo.check(); + assert.equal(result.ok, false); + const details = result.problems.map((problem) => problem.detail); + assert.ok(details.some((detail) => detail.includes('a.json: a wired fixture must not carry gapStage')), details.join('\n')); + assert.ok(details.some((detail) => detail.includes('b.json: a known gap must describe')), details.join('\n')); +}); + +test('evidence must point at files that exist in the repository', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeManifest([ + entry('a.json', { + coreEvidence: ['rust/lithe-core/src/tests/gone.rs:12 vanished'], + windowsEvidence: [fixtureEvidence('a.json')], + }), + ]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['evidence-path']); + assert.match(result.problems[0].detail, /rust\/lithe-core\/src\/tests\/gone\.rs/); +}); + +test('fixture paths must stay inside the fixtures directory', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeManifest([entry('../outside.json')]); + const result = repo.check(); + assert.equal(result.ok, false); + assert.ok(kinds(result).includes('invalid-entry'), kinds(result).join(', ')); + assert.ok(result.problems.some((problem) => /stay inside the fixtures directory/.test(problem.detail))); + // The escaping path must not silently pass the set comparison either. + assert.ok(kinds(result).includes('unknown-fixture'), 'a.json on disk goes unclassified'); + assert.ok(kinds(result).includes('missing-fixture'), 'the escaped entry matches no file'); +}); + +test('a missing fixtures directory is an io problem, not a crash', (t) => { + const root = mkdtempSync(join(tmpdir(), 'lithe-fixture-coverage-test-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const result = runCoverageCheck({ + repoRoot: root, + fixturesDir: join(root, 'does-not-exist'), + manifest: { schemaVersion: 1, fixtures: [] }, + }); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['io']); + assert.equal(result.summary, null); +}); + +test('a non-object manifest is an io problem', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + const result = runCoverageCheck({ repoRoot: repo.root, fixturesDir: repo.fixturesDir, manifest: [] }); + assert.equal(result.ok, false); + assert.deepEqual(kinds(result), ['io']); +}); + +test('enumeration is recursive, uses / separators, and is sorted', (t) => { + const repo = tempRepo(t); + repo.writeFixture('zeta.json'); + repo.writeFixture('nested/deep/gamma.json'); + repo.writeFixture('alpha.json'); + assert.deepEqual(enumerateFixtureFiles(repo.fixturesDir), [ + 'alpha.json', + 'nested/deep/gamma.json', + 'zeta.json', + ]); +}); + +test('evidence parsing keeps the file part and drops the line number', () => { + assert.equal(evidenceFilePath('rust/lithe-core/src/tests/vcs.rs:65 a name'), 'rust/lithe-core/src/tests/vcs.rs'); + assert.equal(evidenceFilePath('windows/tauri/src-tauri/src/plugin_host/tests.rs name'), 'windows/tauri/src-tauri/src/plugin_host/tests.rs'); + assert.equal(evidenceFilePath('shared/contracts/plugins/v1/fixtures/x.json'), 'shared/contracts/plugins/v1/fixtures/x.json'); +}); + +// The enforcement test: the real manifest must match the real fixtures +// directory right now, and its known gaps must stay explicit. When this test +// fails, classify the new fixture or clean the stale entry - never weaken +// this assertion to make a change pass. +test('the real fixture directory and classification list agree', () => { + const repoRoot = defaultRepoRoot(); + const manifest = JSON.parse(readFileSync(defaultManifestPath(), 'utf8')); + const fixturesDir = join(repoRoot, ...manifest.fixturesDirectory.split('/')); + const result = runCoverageCheck({ repoRoot, fixturesDir, manifest }); + assert.equal(result.ok, true, result.problems.map((problem) => `${problem.kind}: ${problem.detail}`).join('\n')); + assert.ok(result.summary.fixtures >= 30, `expected the full fixture set, got ${result.summary.fixtures}`); + assert.ok(result.summary.knownGaps > 0, 'the audit documents real gaps; a zero-gap manifest means the statuses were flattened'); + for (const fixture of manifest.fixtures) { + if (fixture.windowsStatus !== 'wired') { + assert.match(fixture.gapStage, /^M(10|[1-9])$/, `${fixture.path} gap stage`); + assert.ok(fixture.gapDetail.trim().length > 0, `${fixture.path} gap detail`); + } + } +}); diff --git a/scripts/plugin-fixture-coverage.json b/scripts/plugin-fixture-coverage.json new file mode 100644 index 000000000..6971c008e --- /dev/null +++ b/scripts/plugin-fixture-coverage.json @@ -0,0 +1,521 @@ +{ + "schemaVersion": 1, + "description": "Explicit classification of every file under shared/contracts/plugins/v1/fixtures. Checked by scripts/check-plugin-fixture-coverage.mjs: the directory enumeration and this list must match exactly. windowsStatus: wired = a product-path Windows test exercises this fixture; validated-only = only manifest validation/gating; indirect-core = Windows consumes it through Core commands without reading the file; unwired = no Windows list or test names it. A non-wired status is a known gap and must keep gapStage/gapDetail until a real test closes it. Evidence strings are '[:] '; the checker verifies the file exists, lines are documentation as of base commit 1e78b59b6385fca6a881639b42048bf3bc587f4a.", + "fixturesDirectory": "shared/contracts/plugins/v1/fixtures", + "fixtures": [ + { + "path": "build-command-output-file.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (13 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M4", + "gapDetail": "Windows 只做到清单验收与能力门控;输出文件模型解码(${outputFile}、JUnit 路径)没有任何宿主接线测试,属声明式构建接入。" + }, + { + "path": "build-command-stdout.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/build_systems.rs:1071 resolve_options_describe_explicit_empty_text_values", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (9 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M4", + "gapDetail": "Windows 只有清单验收与能力门控;stdout 模型解码、选项解析与检测优先级无宿主接线测试,属声明式构建接入。" + }, + { + "path": "build-host-integration.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/build_systems.rs:482 maven_reactor_detects_through_the_host_integration", + "rust/lithe-core/src/tests/build_runtime_versions.rs:15 host_runtime_probe_arguments_are_portable_across_platforms", + "rust/lithe-core/src/tests/project_generators.rs:92 catalog_lists_generators_with_requirement_providers", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (5 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M4", + "gapDetail": "Windows 只有清单验收与能力门控;hostIntegration 模型来源(Maven reactor 探测、运行时探测参数)无宿主接线测试,属声明式构建接入。" + }, + { + "path": "build-plans.json", + "category": "build-plans", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M4", + "gapDetail": "Windows 无直接读取;声明式构建接入(M4)时,宿主模型/任务/测试/诊断行为覆盖应以本 golden 对齐。" + }, + { + "path": "build-provider-language-server.plugin.json", + "category": "valid-manifest", + "outcome": "partial-support", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/build_systems.rs:1460 launch_plans_name_exactly_one_process", + "rust/lithe-core/src/tests/declarative_plugins.rs:366 level_3_manifests_list_exactly_the_capabilities_they_use", + "rust/lithe-core/src/tests/declarative_plugins.rs:445 build_providers_require_their_own_host_capability", + "rust/lithe-core/src/tests/marketplace_plugins.rs:219 install_review_fixtures_match_golden_outcomes" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:80 invalid_manifest_fixtures_keep_their_stable_codes (only read indirectly as 4 case bases)" + ], + "windowsStatus": "unwired", + "gapStage": "M5", + "gapDetail": "不在 Windows 任何合法/门控清单中,仅作为 invalid-manifests 用例基底被间接读取;连'被能力门控拒绝'这一断言都没有专属覆盖。共享语言服务器 Provider 会话属 M5。" + }, + { + "path": "build-provider.plugin.json", + "category": "valid-manifest", + "outcome": "partial-support", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/build_systems.rs:1115 provider_processes_are_install_review_programs", + "rust/lithe-core/src/tests/declarative_plugins.rs:445 build_providers_require_their_own_host_capability", + "rust/lithe-core/src/tests/marketplace_plugins.rs:219 install_review_fixtures_match_golden_outcomes", + "rust/lithe-core/src/tests/marketplace_plugins.rs:332 build_provider_versions_require_host_support_before_download" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M5", + "gapDetail": "Windows 只断言该清单被能力门控拒绝;自有进程 Provider 的启动、litheBuild 协商、会话回收没有任何宿主测试,属 M5。" + }, + { + "path": "build-static-file.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:128 build_fixtures_validate", + "rust/lithe-core/src/tests/build_systems.rs:143 build_plans_match_the_contract_goldens", + "rust/lithe-core/src/tests/build_systems.rs:218 cargo_run_entries_match_the_builtin_detector", + "rust/lithe-core/src/tests/build_systems.rs:361 detection_applies_nesting_priority_and_rejections", + "rust/lithe-core/src/tests/build_systems.rs:1236 static_models_and_sync_fingerprints_reject_escaping_symlinks", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (33 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M4", + "gapDetail": "Windows 只有清单验收与能力门控;static 模型检测、选项、任务、同步指纹无宿主接线测试,属声明式构建接入。" + }, + { + "path": "build-tool-path.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/build_systems.rs:1599 explicit_build_tool_path_requires_supported_global_file_setting", + "rust/lithe-core/src/tests/build_systems.rs:1666 executable_candidates_order_and_constraints_are_shared_and_deterministic", + "rust/lithe-core/src/tests/build_systems.rs:1719 published_build_v2_paths_adapt_to_candidates_and_preserve_invalid_values", + "rust/lithe-core/src/tests/build_systems.rs:1755 executable_resolution_is_opt_in_and_custom_runtimes_share_the_same_plan" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M4", + "gapDetail": "Windows 侧零引用:不在合法/门控清单,也没有任何宿主测试读取;executableResolution:1 候选顺序与显式路径语义属 M4 接入。" + }, + { + "path": "command-plans.json", + "category": "command-plans", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:212 command_plan_fixtures_match_golden_expansions" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M5", + "gapDetail": "Windows 经 Core 命令消费展开结果;bundled_php_contract_package_expands_plans 实测了 runPlan/testPlan 展开但用的是 PHP 契约包而非本 golden。发现/测试/launchPlan 作用域的宿主行为测试属 M5。" + }, + { + "path": "function-protocol.json", + "category": "function-protocol", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/lsp/interface/plugin_function.rs:441 the_offer_matches_the_fixture", + "rust/lithe-core/src/lsp/interface/plugin_function.rs:528 outcomes_follow_the_fixture" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M6", + "gapDetail": "Windows 网页桥 v0 对 functions.* 返回 plugin.web.unknownMethod(web.rs 已知边界);函数列表/调用/取消/进度的宿主接线属 M6。" + }, + { + "path": "hello-lithe.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:532 ui_contributions_are_normalized_without_interpreting_text", + "rust/lithe-core/src/tests/declarative_plugins.rs:610 ui_icon_declarations_validate_and_preserve_theme_resources", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (12 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/web.rs:1657 text_window_is_literal_and_hello_fixture_is_registered", + "windows/tauri/src/features/plugins/declarative-plugin-ui.test.ts:33 hello ui1 contributes a text window and all three command placements and revokes them" + ], + "windowsStatus": "wired", + "gapStage": null, + "gapDetail": null + }, + { + "path": "invalid-manifests.json", + "category": "invalid-edits", + "outcome": "rejection", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:80 invalid_manifest_fixtures_keep_their_stable_codes" + ], + "windowsStatus": "wired", + "gapStage": null, + "gapDetail": null + }, + { + "path": "language-host-integration.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:152 host_integration_capabilities_are_normalized_and_checked_against_the_integration", + "rust/lithe-core/src/tests/declarative_plugins.rs:276 runtime_launch_and_initialization_options_are_reviewed_programs", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (13 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities" + ], + "windowsStatus": "validated-only", + "gapStage": "M10", + "gapDetail": "Windows 只做清单验收;jdt 集成的启动计划解析、必需包资源校验与真实包接线没有 fixture 级宿主测试(launch.rs 测试使用 language-packaged-server),真实 JDT 包验收随 M10 第一阶段。" + }, + { + "path": "language-packaged-server.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:81 validation_normalizes_order_and_spelling_without_touching_arguments", + "rust/lithe-core/src/tests/declarative_plugins.rs:251 api_level_1_language_server_arguments_stay_literal", + "rust/lithe-core/src/tests/marketplace_plugins.rs:219 install_review_fixtures_match_golden_outcomes", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (23 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:162 scan_accepts_valid_packages_and_quarantines_broken_ones", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:196 store_problems_quarantine_packages_with_valid_manifests", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:258 catalog_build_combines_bundled_and_installed_sources", + "windows/tauri/src-tauri/src/plugin_host/install_tests.rs:43 directory_snapshot_copies_contents", + "windows/tauri/src-tauri/src/plugin_host/install_tests.rs:118 staging_install_replaces_version_and_rejects_broken_manifests", + "windows/tauri/src-tauri/src/plugin_host/install_tests.rs:185 rollback_picks_the_highest_previous_version", + "windows/tauri/src-tauri/src/plugin_host/launch.rs:933 resolve_launch_consumes_the_real_core_response_envelope" + ], + "windowsStatus": "wired", + "gapStage": null, + "gapDetail": null + }, + { + "path": "language-system-command.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:106 validation_keeps_the_leading_dot_of_hidden_file_names", + "rust/lithe-core/src/tests/declarative_plugins.rs:330 validation_probe_cannot_use_settings", + "rust/lithe-core/src/tests/marketplace_plugins.rs:219 install_review_fixtures_match_golden_outcomes", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (1 case uses this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:258 catalog_build_combines_bundled_and_installed_sources" + ], + "windowsStatus": "validated-only", + "gapStage": "M10", + "gapDetail": "Windows 覆盖清单验收与 bundled catalog 扫描,但 systemCommand 启动(PATH/工具发现解析、LITHE_*_PATH 导出)没有 fixture 级宿主测试;真实 systemCommand 插件(gopls/intelephense)验收随 M10 第一阶段。macOS 侧同样没有直接引用。" + }, + { + "path": "marketplace/index-cases.json", + "category": "marketplace", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/marketplace_plugins.rs:100 index_fixture_cases_match_expected_availability" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M2", + "gapDetail": "Windows 声明式市场宿主未接(M2);当前无任何 Windows 测试消费可用性判定结果。" + }, + { + "path": "marketplace/index.json", + "category": "marketplace", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/marketplace_plugins.rs:100 index_fixture_cases_match_expected_availability", + "rust/lithe-core/src/tests/marketplace_plugins.rs:131 accepted_entries_are_normalized_newest_first", + "rust/lithe-core/src/tests/marketplace_plugins.rs:272 ui_api_availability_requires_an_implemented_platform_host", + "rust/lithe-core/src/tests/marketplace_plugins.rs:332 build_provider_versions_require_host_support_before_download" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M2", + "gapDetail": "Windows 声明式市场宿主未接(M2);索引归一化与可用性只经 Core 命令验证。" + }, + { + "path": "marketplace/install-reviews.json", + "category": "marketplace", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/marketplace_plugins.rs:219 install_review_fixtures_match_golden_outcomes" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M2", + "gapDetail": "Windows 磁盘安装链走 installReview 命令但测试用 language-packaged-server 清单而非本 golden 审查矩阵;完整来源/审批记录接入属 M2。" + }, + { + "path": "marketplace/invalid-index-entries.json", + "category": "marketplace", + "outcome": "rejection", + "coreEvidence": [ + "rust/lithe-core/src/tests/marketplace_plugins.rs:147 invalid_index_entries_report_their_stable_codes" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M2", + "gapDetail": "Windows 无市场索引消费路径(M2);拒绝码一致性的宿主断言未建立。" + }, + { + "path": "marketplace/source-urls.json", + "category": "marketplace", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/marketplace_plugins.rs:203 source_url_fixtures_resolve_to_expected_index_urls" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M2", + "gapDetail": "Windows 无源解析入口(M2)。" + }, + { + "path": "native-controls.plugin.json", + "category": "native-ui", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:827 native_public_controls_validate_types_ranges_info_and_buttons" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M7", + "gapDetail": "Windows 侧零引用;ui:3 原生控件渲染未声明也未实现,不能用网页替代。" + }, + { + "path": "native-form.plugin.json", + "category": "native-ui", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:721 native_ui_forms_validate_controls_values_actions_and_budgets", + "rust/lithe-core/src/tests/declarative_plugins.rs:785 native_ui_form_grid_validates_columns_spans_and_scope" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M7", + "gapDetail": "Windows 侧零引用;表单控件、值恢复与提交语义属 ui:3(M7)。" + }, + { + "path": "native-panel.plugin.json", + "category": "native-ui", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:666 native_ui_validates_version_ownership_actions_and_budgets", + "rust/lithe-core/src/tests/declarative_plugins.rs:785 native_ui_form_grid_validates_columns_spans_and_scope" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M7", + "gapDetail": "Windows 侧零引用;原生树/列表/工具栏与状态恢复属 ui:3(M7)。" + }, + { + "path": "node-runtime.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:276 runtime_launch_and_initialization_options_are_reviewed_programs", + "rust/lithe-core/src/tests/declarative_plugins.rs:351 settings_are_normalized_in_identifier_order", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (24 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities" + ], + "windowsStatus": "validated-only", + "gapStage": "M10", + "gapDetail": "Windows 只做清单验收;runtime 启动(packagedRuntime/minimumVersion 探测)与设置解析的 fixture 级宿主测试缺失(PHP 契约包端到端覆盖了 runtime node 计划展开,但不是本清单),真实插件验收随 M10 第一阶段。" + }, + { + "path": "project-generator-templates/.gitattributes", + "category": "template-assets", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match (plan.rs collect_files walks the templates directory)" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M8", + "gapDetail": "模板字节稳定性标记(* -text),经 plan.rs 递归读取被 Core golden 测试消费;Windows 归档解压/模板生成接入属 M8。" + }, + { + "path": "project-generator-templates/app/README.md", + "category": "template-assets", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match (plan.rs collect_files walks the templates directory)" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M8", + "gapDetail": "files 步骤模板,经 plan.rs 递归读取被 Core golden 测试消费;Windows 生成链路接入属 M8。" + }, + { + "path": "project-generator-templates/app/dot-gitignore", + "category": "template-assets", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match (plan.rs collect_files walks the templates directory)" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M8", + "gapDetail": "files 步骤模板(dot-gitignore 命名规避 git 忽略),经 plan.rs 递归读取被 Core golden 测试消费;Windows 生成链路接入属 M8。" + }, + { + "path": "project-generator-templates/main/Main.java", + "category": "template-assets", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match (plan.rs collect_files walks the templates directory)" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M8", + "gapDetail": "files 步骤模板(Main.java 带条件占位符),经 plan.rs 递归读取被 Core golden 测试消费;Windows 生成链路接入属 M8。" + }, + { + "path": "project-generator.plugin.json", + "category": "project-generation", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match", + "rust/lithe-core/src/tests/project_generators.rs:92 catalog_lists_generators_with_requirement_providers", + "rust/lithe-core/src/tests/project_generators.rs:259 rejects_invalid_generator_declarations", + "rust/lithe-core/src/tests/project_generators.rs:327 project_generators_need_api_level_3", + "rust/lithe-core/src/tests/project_generators.rs:339 generator_commands_require_install_review_again" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M8", + "gapDetail": "Windows 侧零引用;新建项目窗口、生成计划与 staging 清理未接入。" + }, + { + "path": "project-plans.json", + "category": "project-plans", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/project_generators.rs:65 golden_project_plans_match" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M8", + "gapDetail": "Windows 经 Core 命令消费生成计划;宿主生成协调器与失败清理测试属 M8。" + }, + { + "path": "runtime-versions.json", + "category": "runtime-versions", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:312 runtime_version_fixtures_match_golden_outcomes" + ], + "windowsEvidence": [], + "windowsStatus": "indirect-core", + "gapStage": "M10", + "gapDetail": "Windows launch.rs 用 node/java/go 真实输出格式自测版本解析,未读取本 golden;建议接入宿主探测路径时用本 fixture 对齐,真实运行时验收随 M10。" + }, + { + "path": "settings-v2.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:901 immediate_settings_must_be_used_by_workspace_configuration", + "rust/lithe-core/src/tests/declarative_plugins.rs:911 settings_v2_manifests_validate_and_resolve_by_layer", + "rust/lithe-core/src/tests/declarative_plugins.rs:958 settings_v2_rejects_unsafe_setting_use" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities", + "windows/tauri/src-tauri/src/plugin_host/tests.rs:128 host_v0_capabilities_gate_unimplemented_sections" + ], + "windowsStatus": "validated-only", + "gapStage": "M4", + "gapDetail": "settings 2/languageServers 2 已在宿主能力声明内,但该 fixture 同时 requires buildSystems/buildProviders,v0 宿主因此整体门控(tests.rs:149 的注释把门控原因写成'settings 2 与 languageServers 2 未声明',与 host_capabilities 实际声明不符,属过时注释)。注册设置页、settingActions 与分层覆盖的宿主行为测试缺 settings-v2 fixture 级覆盖(M4/M6 补齐;现有覆盖走声明式设置编辑器与 settings_tests)。" + }, + { + "path": "vcs-provider.plugin.json", + "category": "vcs-provider", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/vcs.rs:65 the_provider_fixture_validates_and_is_reviewed_with_its_tool", + "rust/lithe-core/src/tests/vcs.rs:94 provider_manifests_state_permissions_activation_and_requirements", + "rust/lithe-core/src/tests/vcs.rs:147 a_launch_plan_names_the_vcs_provider_it_starts", + "rust/lithe-core/src/tests/vcs.rs:196 root_detection_matches_the_contract_cases" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "M9", + "gapDetail": "Windows 侧零引用;VCS Provider 会话、根识别与宿主入口未接入(macOS 也未声明 vcsProviders)。" + }, + { + "path": "web-panel.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:65 contract_fixtures_and_official_plugins_validate", + "rust/lithe-core/src/tests/declarative_plugins.rs:563 web_tool_windows_need_ui_2_their_permission_and_a_package_page", + "rust/lithe-core/src/tests/declarative_plugins.rs:130 invalid_manifest_fixtures_report_their_stable_codes (8 cases use this base)" + ], + "windowsEvidence": [ + "windows/tauri/src-tauri/src/plugin_host/tests.rs:112 valid_fixtures_validate_without_host_capabilities" + ], + "windowsStatus": "validated-only", + "gapStage": "M6", + "gapDetail": "Windows 宿主已声明 ui:[1,2] 并具备 Web 工具窗口链路(web.rs 机制测试),但本 fixture 声明的包页面/入口没有被任何宿主测试打开;build.*/functions.* 桥方法按 v0 边界拒绝,接入属 M4/M6。" + } + ] +} From 215542c5d196ab66f92d094b817b1fdc5bda1f1c Mon Sep 17 00:00:00 2001 From: Mucheen <1528136628@qq.com> Date: Sun, 25 Oct 2026 08:32:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(plugins):=20=E6=A0=A1=E6=AD=A3=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E4=BA=8B=E5=AE=9E=E5=B9=B6=E6=8E=A5=E9=80=9A=20fixtur?= =?UTF-8?q?e=20=E8=A6=86=E7=9B=96=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 验收矩阵按基线代码纠正:macOS 宿主能力表(PluginTypes.swift:492-504)已 声明 buildProviders:[1]、buildSystems:[1,2]、executableResolution:[1]、 executionProviders:[1]、projectGenerators:[1]、ui:[1,2,3]、vcsProviders:[1] (preview 另加 backend:[1]),maven/cargo/svn 在 macOS 可装且矩阵记 implemented|partial/验证 pending;删除'契约只到 buildSystems:1'等过时结论, 补记商店新增 database 插件(backend 属冻结范围外新能力) - 检测器:gapStage 允许 M0 与 followup(M11 仍拒绝);预置 signature-vectors 类别并允许其 coreEvidence 为空(双端宿主测试消费,无 Core 校验面) - 测试:移除真实仓 knownGaps>0 永久缺口断言,改为全 wired 正例 + 诚实分类 负例;新增 M0/followup、签名向量豁免与 CI 接线守卫单测(共 18 项) - CI:ci-macos.yml changes job(ubuntu,Draft 也运行)新增检测器 CLI、 node --test --test-timeout=15000 与 junit→HTML 计时报告三步,复用既有 artifact 上传;新增 testing Note 记录门禁与测试策略 - 同步 preview(83017afa):登记 backend.plugin.json(unwired/followup), invalid-manifests 更新为 163 用例/13 基底,function-protocol v2 证据行号 更新;M1 #1476 合入后仅需按 Note 中最小条目登记 signature-vectors.json --- ...2026-10-25-plugin-fixture-coverage-gate.md | 82 +++++++++++++ .github/workflows/ci-macos.yml | 29 +++++ docs/development/windows-plugin-acceptance.md | 15 +-- .../windows-plugin-fixture-coverage.md | 24 ++-- scripts/check-plugin-fixture-coverage.mjs | 20 +++- .../check-plugin-fixture-coverage.test.mjs | 110 +++++++++++++++++- scripts/plugin-fixture-coverage.json | 22 +++- 7 files changed, 271 insertions(+), 31 deletions(-) create mode 100644 .agents/notes/implemented/testing/2026-10-25-plugin-fixture-coverage-gate.md diff --git a/.agents/notes/implemented/testing/2026-10-25-plugin-fixture-coverage-gate.md b/.agents/notes/implemented/testing/2026-10-25-plugin-fixture-coverage-gate.md new file mode 100644 index 000000000..174641691 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-25-plugin-fixture-coverage-gate.md @@ -0,0 +1,82 @@ +# Agent 笔记:插件 fixture 覆盖门禁 + +状态:已实现 + +## 先说结论 + +`shared/contracts/plugins/v1/fixtures` 的覆盖检测器(`scripts/check-plugin-fixture-coverage.mjs` + `plugin-fixture-coverage.json` + 单测)已接入 `ci-macos.yml` 的 `changes` job——该 job 在 ubuntu runner 上对每个 pull request(含 Draft)运行,不新增 workflow、不占 macOS/Windows runner。任何人新增 fixture 而不在清单登记,真实 PR 检查会以 `unknown-fixture` 失败。门禁的防伪造立场是"证据与诚实分类",不是"永远保持 partial":允许全部 fixture 真实 wired 后零缺口通过,但没有证据的 wired、没有阶段归属的缺口一律拒绝。 + +## 问题 + +- 覆盖清单(M0 交付)靠显式登记维护。若只在本地运行检测器,"新增 fixture 忘了登记"只会安静漏过,`unknown-fixture` 不会让任何 PR 失败,清单会重新漂移。 +- 第一版真实仓库守护测试断言 `knownGaps > 0`:把 2026-10-25 当天的真实缺口数量固化成了永久约束。未来某天缺口真实补齐时这个断言必然失败,反而诱导后人删掉守护测试。 +- 缺口阶段(gapStage)的注释说 M0–M10,正则却排除 M0,语义与说明不一致。 + +## 决策 + +### CI 入口复用现有 Linux lane,不旁建 workflow + +`ci-macos.yml` 的 `changes` job 已有一个"scripts 测试 + JUnit/HTML 计时报告 + artifact 上传"的成熟 lane(Sparkle baseline 模式):`node --test` 双 reporter(spec 到 stdout、junit 到 `.artifacts/test-stability/.junit.xml`),再用 `parse-junit-cases.mjs` + `writeTestReportArtifacts` 生成逐项计时的 HTML/JUnit 报告,最后由同一 job 的 upload 步骤上传整个 `.artifacts/test-stability/`。插件 fixture 门禁镜像同一模式插入三步:检测器 CLI(真正的清单 vs 目录比较)、单测(`--test-timeout=15000` 本地期限)、计时报告生成。该 job 在 ubuntu 上对所有 PR 运行(含 Draft),因此 #1476 这类新增 fixture 的 PR 在合入前就会撞上 `unknown-fixture`。 + +门禁与测试的接线由测试自身守护:`check-plugin-fixture-coverage.test.mjs` 里有一条 CI 接线守卫测试,读取 `ci-macos.yml` 断言两个入口(CLI 步骤、测试文件)与期限/报告参数存在——workflow 改名或移除调用时,测试先红。 + +### 防伪造 = 诚实分类,不是永久 partial + +- 正例:一个全部 wired 且证据文件真实存在的清单,检测器必须通过且 `knownGaps = 0`(有专门单测锁住这条路)。未来覆盖补齐时门禁自然放行。 +- 负例:wired 却没有 Windows 证据、known gap 缺阶段或说明、证据引用文件不存在,全部拒绝(既有单测覆盖)。 +- 真实仓库守护测试只断言"清单与目录一致 + 每个非 wired 条目带合法阶段与说明",不再断言缺口数量。 + +### gapStage 语义:M0–M10 全部合法 + +路线图 M0 的退出条件把基线问题路由进聚焦修复 PR,所以"由 M0 拥有的缺口"是合法归属;M11 及以后不存在。正则为 `^M(10|[0-9])$`,注释与实现一致,并有单测同时覆盖 M0 接受与 M11 拒绝。 + +### M1 交接(#1476) + +`fixtures/signature-vectors.json` 在 #1476 中新增;其 PR 合入 preview 前(或同时)必须在清单登记,否则撞 `unknown-fixture`。最小登记条目(检测器已预置 `signature-vectors` 类别): + +```json +{ + "path": "signature-vectors.json", + "category": "signature-vectors", + "outcome": "success", + "coreEvidence": [], + "windowsEvidence": ["windows/tauri/src-tauri/src/plugin_host/signature.rs:663 shared_fixture_verifies_end_to_end"], + "windowsStatus": "<按 #1476 实际证据填 wired 或 validated-only>", + "gapStage": null, + "gapDetail": null +} +``` + +该类别是唯一允许 `coreEvidence` 为空的类别:签名向量由双端宿主各自的签名测试消费(Windows `signature.rs`、macOS `PluginSignatureVectorTests.swift` 的 RFC 8032 锚点、确定性签名、内容摘要、签名文档回读四组 @Test),没有 Core 校验面;豁免只对 `signature-vectors` 生效,其他类别仍必须有 Core 侧证据(有单测锁定)。`backend.plugin.json` 已由本 PR 随 preview 同步(`83017afa`/8471b998)登记为 `unwired` + `followup`——`followup` 是为"冻结路线图(M0–M10)之后落地、按计划 §6 单列后续阶段"的新能力保留的缺口阶段,是 M 编号之外的唯一合法值。登记条目的最终行号以 #1476 合并时的代码为准。合并顺序优先 #1474(门禁)先合、#1476 随后带登记合入;若 #1476 先合,它必须自带登记条目,否则其 PR 门禁失败。 + +## 考虑过的备选方案 + +- 新建独立 `ci-fixture-coverage.yml`:多一个 workflow、多一份维护与触发面,且与"优先复用现有门禁"的仓库惯例冲突,不采用。 +- 只在本地可选运行检测器:登记纪律依赖人的记忆,已知会漂移,不采用。 +- 保留 `knownGaps > 0` 作为防伪造断言:把"今天有缺口"固化成"永远有缺口",未来真实补齐反而红,防伪造应针对"没有证据的 wired",不采用。 +- 在 `verify-shared-contracts.sh`(Ruby 内联)里实现枚举比较:该脚本是契约语义校验(schema/拒绝码),把 Node 已实现的检测器复制一份 Ruby 只会分叉逻辑,不采用。 + +## 后果 + +- 收益:新增 fixture 未登记会直接打红 PR(含 Draft);单测有 15 秒本地期限与逐项计时报告;覆盖补齐路径(全 wired)有正例保证畅通。 +- 代价:`ci-macos.yml` 的 `changes` job 多两步(约几秒);维护清单成为改 fixture 的同 PR 义务;workflow 与测试之间由守卫测试双向绑定,改 CI 入口措辞时守卫测试需要同步。 + +## 验证 + +```bash +node scripts/check-plugin-fixture-coverage.mjs +node --test --test-timeout=15000 scripts/check-plugin-fixture-coverage.test.mjs +node --test --test-reporter=junit --test-reporter-destination=.artifacts/test-stability/plugin-fixture-coverage.junit.xml scripts/check-plugin-fixture-coverage.test.mjs +./.agents/skills/write-stable-tests/scripts/verify-test-stability.ps1 # 或 .sh +node scripts/verify-agent-notes.mjs +``` + +CI 等效路径:`ci-macos.yml` changes job(ubuntu)的 "Check plugin fixture coverage manifest" / "Run plugin fixture coverage tests" / "Generate plugin fixture coverage timing report" 三步,artifact 随既有 upload 步骤上传。 + +## 适用范围 + +- `scripts/check-plugin-fixture-coverage.mjs`、`scripts/plugin-fixture-coverage.json`、`scripts/check-plugin-fixture-coverage.test.mjs` +- `.github/workflows/ci-macos.yml` 的 changes job 中 "plugin fixture coverage" 三步 +- `docs/development/windows-plugin-fixture-coverage.md`(审计快照与维护规则) + +不覆盖:M1 拥有的签名/安装信任链 Note(#1476)、共享契约 fixtures 本体、平台功能矩阵。 diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 48c414021..71a9b442d 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -76,6 +76,35 @@ jobs: writeTestReportArtifacts(`${stem}.json`); NODE + # Declarative-plugin fixture coverage: the directory enumeration and + # the explicit classification list must match, so a fixture added + # without registration fails every pull request on this Linux lane. + - name: Check plugin fixture coverage manifest + timeout-minutes: 2 + run: node scripts/check-plugin-fixture-coverage.mjs + + - name: Run plugin fixture coverage tests + timeout-minutes: 2 + run: | + node --test --test-timeout=15000 \ + --test-reporter=spec --test-reporter-destination=stdout \ + --test-reporter=junit --test-reporter-destination=.artifacts/test-stability/plugin-fixture-coverage.junit.xml \ + scripts/check-plugin-fixture-coverage.test.mjs + + - name: Generate plugin fixture coverage timing report + if: always() + run: | + node --input-type=module <<'NODE' + import fs from 'node:fs'; + import { parseJUnitCases } from './.agents/skills/write-stable-tests/scripts/parse-junit-cases.mjs'; + import { writeTestReportArtifacts } from './.agents/skills/write-stable-tests/scripts/generate-test-report.mjs'; + const stem = '.artifacts/test-stability/plugin-fixture-coverage'; + const tests = parseJUnitCases(fs.readFileSync(`${stem}.junit.xml`, 'utf8')); + if (tests.length === 0) throw new Error('Missing plugin fixture coverage test results'); + fs.writeFileSync(`${stem}.json`, JSON.stringify({ runner: 'node', warnMs: 1000, maxMs: 15000, tests })); + writeTestReportArtifacts(`${stem}.json`); + NODE + - name: Upload Sparkle download timing report if: always() uses: actions/upload-artifact@v4 diff --git a/docs/development/windows-plugin-acceptance.md b/docs/development/windows-plugin-acceptance.md index a86b50e51..dfcca5c90 100644 --- a/docs/development/windows-plugin-acceptance.md +++ b/docs/development/windows-plugin-acceptance.md @@ -3,7 +3,7 @@ 状态:验收准备资料,不是验收记录。本文按 [Windows 插件功能对齐计划](windows-plugin-parity-plan.md) M10 的要求,固定真实 Windows 插件包的身份、来源与逐项可复现验收步骤,并区分第一阶段(M1–M6)与第二阶段(M7–M9)出口。**本轮没有执行任何一次完整验收,也没有任何一行可以据此宣告 M10 完成。** -数据核实:2026-10-24,gh API(仓库元数据、release 资产、`Lithe-IDEA/Lithe-store/lithe-plugin-index.json`、`Lithe-IDEA/lithe-plugin-docs`)+ 主仓库基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a` 的源码。索引与 release 会持续变化,每次执行验收前必须重新核实并把当时的 tag/版本/SHA-256 写进当次记录,不得沿用本文。 +数据核实:2026-10-24(首次)与 2026-10-25(能力声明/矩阵维度复核,并同步 preview `83017afa`),gh API(仓库元数据、release 资产、`Lithe-IDEA/Lithe-store/lithe-plugin-index.json`、`Lithe-IDEA/lithe-plugin-docs`)+ 主仓库基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a` 的源码。索引与 release 会持续变化,每次执行验收前必须重新核实并把当时的 tag/版本/SHA-256 写进当次记录,不得沿用本文。 ## 1. 通用验收流程(每个真实包逐项执行) @@ -34,11 +34,11 @@ | PhpSupportContract(bundled 契约包) | 主仓库 `Plugins/win/Official/PhpSupportContract`,版本 1.0.0,api2,`sameTeamAsHost`(基线 1e78b59b) | 仓库内 Lithe 资源;`intelephense.js` 为 65 字节占位入口 | runtime launch:node 启动 `language-server/bin/intelephense.js`;无 `${arch}` 路径,两架构同入口 | Node.js(宿主发现);`php` CLI(运行/测试 executable) | 已随宿主安装链验收(validate/launch/runPlan/testPlan/resolveSettings 端到端测试 `tests.rs:433`);真实 Intelephense 替换 pending——其许可禁止再分发(见 `2026-10-20-go-php-community-plugins` Note),Windows 分发方式需另行决定 | M10 第一阶段(完整实机链) | | io.github.lithe-idea.php-support | [lithe-plugin-php](https://github.com/Lithe-IDEA/lithe-plugin-php) tag `v0.1.0`,api1 | Apache-2.0(仓库 license) | systemCommand `intelephense --stdio`;工具架构随用户安装 | Node.js(runtimeDependencies)+ 用户自装 intelephense(npm,许可禁止打包) | macOS 可装;**windows 工件 pending**;官方索引/商店均未列 windows 平台 | M2(市场安装链)→ M10 第一阶段 | | io.github.lithe-idea.go-support | [lithe-plugin-go](https://github.com/Lithe-IDEA/lithe-plugin-go) tag `v0.1.0`,api1 | Apache-2.0 | systemCommand `gopls`;go 工具链架构随系统 | Go 工具链(runtimeDependencies,宿主发现) | macOS 可装;**windows 工件 pending** | M2 → M10 第一阶段 | -| io.github.lithe-idea.lua-support | [lithe-plugin-lua](https://github.com/Lithe-IDEA/lithe-plugin-lua) tag `v0.3.0`(另有 0.1.0/0.2.0),api1 | Apache-2.0 | packaged `bin/lithe-lua-language-server`——**架构相关二进制**,Windows 需按 x64/arm64 分别出工件 | 无(服务器自带) | macOS 可装(0.3.0 起分 arm64/x64 资产);**windows 工件 pending** | M2 → M10 第一阶段 | +| io.github.lithe-idea.lua-support | [lithe-plugin-lua](https://github.com/Lithe-IDEA/lithe-plugin-lua) tag `v0.3.0`(另有 0.1.0/0.2.0),api1 | Apache-2.0 | packaged `bin/lithe-lua-language-server`——**架构相关二进制**,Windows 需按 x64/arm64 分别出工件 | 无(服务器自带) | macOS 可装(0.3.0 起分 darwin-arm64/darwin-x64 两个资产);**windows 工件 pending** | M2 → M10 第一阶段 | | io.github.lithe-idea.gradle-build | [lithe-plugin-gradle](https://github.com/Lithe-IDEA/lithe-plugin-gradle) tag `v0.1.1`,api3,`requires {buildSystems:1}`,模型源 command | Apache-2.0 | 声明式构建工具窗口 + 命令计划;入口依赖构建工具(Gradle wrapper/gradle) | Gradle(项目 wrapper 或 PATH) | macOS 可装;Windows 当前 `apiUnsupported`(宿主未声明 buildSystems);**windows 工件 pending** | M4 开放能力 → M10 第一阶段 | -| io.github.lithe-idea.maven-build | [lithe-plugin-maven](https://github.com/Lithe-IDEA/lithe-plugin-maven) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:1, serverProcess:1}`,模型源 provider | Apache-2.0 | Provider 进程(自有 launch)+ Core litheBuild 协商 | JDK(provider 进程与 Maven 工具链) | macOS 当前不可装(未声明 buildProviders);Windows `apiUnsupported`;**windows 工件 pending** | M5 → M10 第一阶段(Provider/函数线) | -| io.github.lithe-idea.cargo-build | [lithe-plugin-cargo](https://github.com/Lithe-IDEA/lithe-plugin-cargo) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:2, serverProcess:1, settings:2}`,模型源 provider;0.1.0 为 `{buildSystems:1, buildProviders:1}` | Apache-2.0 | Provider 进程;cargo 工具链 | Rust/Cargo 工具链 | 0.1.1 声明 `buildSystems:2`——当前契约版本只定义到 1,需 Core 与宿主先行支持;0.1.0 双端均未开放对应能力;**windows 工件 pending** | M4/M5 → M10 第一阶段;buildSystems:2 属后续契约演进,不在冻结范围 | -| io.github.lithe-idea.svn-support | [lithe-plugin-svn](https://github.com/Lithe-IDEA/lithe-plugin-svn) tag `v0.1.0`,api3,`requires {vcsProviders:1, serverProcess:1}`,vcsTool svn | Apache-2.0 | VCS Provider 进程会话 | svn 命令行客户端(用户提供) | 双端当前均不可装(macOS 宿主也未声明 vcsProviders);**windows 工件 pending** | M9 → M10 第二阶段 | +| io.github.lithe-idea.maven-build | [lithe-plugin-maven](https://github.com/Lithe-IDEA/lithe-plugin-maven) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:1, serverProcess:1}`,模型源 provider | Apache-2.0 | Provider 进程(自有 launch)+ Core litheBuild 协商 | JDK(provider 进程与 Maven 工具链) | macOS 可装:宿主能力表声明 `buildProviders:[1]`、`buildSystems:[1,2]`、`serverProcess:[1]`(`PluginTypes.swift:492-503`),Core `SUPPORTED_CAPABILITIES` 同表;矩阵 `build-provider-refresh-and-tool` 记 macOS implemented/验证 pending(`MacBuildProviderSessionsTests` 覆盖会话自动化)——implemented 不等于已验证;Windows `apiUnsupported`(能力未开放);**windows 工件 pending** | Windows 侧 M5 开放能力 → M10 第一阶段(Provider/函数线);macOS 侧验证对照随 M10 第一阶段 | +| io.github.lithe-idea.cargo-build | [lithe-plugin-cargo](https://github.com/Lithe-IDEA/lithe-plugin-cargo) tag `v0.1.1`,api3,`requires {buildProviders:1, buildSystems:2, serverProcess:1, settings:2}`,模型源 provider;0.1.0 为 `{buildSystems:1, buildProviders:1}` | Apache-2.0 | Provider 进程;cargo 工具链 | Rust/Cargo 工具链 | 0.1.1 的 `buildSystems:2` 由 Core(`capabilities.rs` `SUPPORTED_CAPABILITIES`)与 macOS 宿主能力表(`buildSystems:[1,2]`)共同支持,macOS 可装(矩阵 `cargo-build-plugin` 记 macOS implemented/验证 pending);Windows `apiUnsupported`(buildSystems/buildProviders 未声明);**windows 工件 pending** | Windows 侧 M4/M5 开放能力(路线图 M4 明确含 `buildSystems:2` 兼容输入解析)→ M10 第一阶段 | +| io.github.lithe-idea.svn-support | [lithe-plugin-svn](https://github.com/Lithe-IDEA/lithe-plugin-svn) tag `v0.1.0`,api3,`requires {vcsProviders:1, serverProcess:1}`,vcsTool svn | Apache-2.0 | VCS Provider 进程会话 | svn 命令行客户端(用户提供) | macOS 可装:宿主已声明 `vcsProviders:[1]`(`PluginTypes.swift:502`),Core 支持;矩阵 `vcs-subversion-and-plugin-providers` 记 macOS partial/验证 pending(`VcsProviderHostTests` 覆盖根识别、元数据监听与生命周期自动化;svn 解析与错误码由其插件仓库 CI 经 Core 桥接在 file:// 仓库验证);Windows `apiUnsupported`;**windows 工件 pending** | M9 → M10 第二阶段 | | Java Support(dev.lithe.plugin.java-support) | macOS bundled 官方包;JDT LS 上游固定 [v1.61.0](https://github.com/eclipse-jdtls/eclipse.jdt.ls/tree/v1.61.0);包内含 `LICENSE-EPL-2.0.txt`(组装脚本 `assemble-package.sh` 随包携带) | JDT LS 及 Eclipse 扩展为 EPL-2.0(随包 LICENSE 文件);icons 目录 Apache-2.0 | Windows 无分发:路线图 M8 明确必须满足其整份 `requires`,不得复用 macOS ZIP(含 macOS 架构 JDK/runtime) | 打包 JDK(packagedRuntime,`${arch}` 路径) | macOS bundled 可用;**Windows 分发 pending(构建/打包决策未做)** | M8(生成器/分发)+ M10 第一阶段(语言链验收) | | 官方可下载插件渠道 | `Plugins/marketplace/index.json` 与 `official-plugins.json` 均为空;发布管线 `release-official-plugins.yml` 存在(macos-26 runner 构建、tag 系列 ``) | 按未来包而定 | 官方索引当前无条目,无可验收的官方 Windows 包 | — | 渠道就绪但**无条目 → pending**;第一个官方包发布后按 §1 全链验收 | M2(索引/下载)→ M10 第一阶段 | | 原生 UI 真实示例(ui:3) | 未选定外部示例仓库与版本 | 待选型 | native schema 树/列表/表单渲染 | 待选型 | **pending 选型**;路线图要求"固定版本的外部示例" | M7 → M10 第二阶段 | @@ -46,8 +46,9 @@ 补充说明: -- 商店索引当前 7 个插件、12 个版本全部只列 `darwin-arm64`/`darwin-x64` 下载(gh 核实 `lithe-plugin-index.json`)。Windows 可用性在这些包发布 windows-\* 工件并更新索引之前,一律按 `plugin.index.platformUnsupported` 展示——这是契约定义的诚实状态,不能通过放宽校验绕过。 -- `cargo-build 0.1.1` 的 `buildSystems:2` 是矩阵里唯一超出当前契约定义版本的字段;记录于此仅作事实核对,不构成本轮工作项。 +- 商店索引当前 8 个插件、13 个版本全部只列 `darwin-arm64`/`darwin-x64` 下载(gh 复核 2026-10-25,`lithe-plugin-index.json`)。Windows 可用性在这些包发布 windows-\* 工件并更新索引之前,一律按 `plugin.index.platformUnsupported` 展示——这是契约定义的诚实状态,不能通过放宽校验绕过。 +- 索引新增的 `io.github.lithe-idea.database 0.1.0` 声明 `requires {backend:1, serverProcess:1, ui:2}`:本审计基线 `1e78b59b` 的 Core 未定义 `backend` capability;preview 已在 `8471b998`(#1470 数据库插件线)新增 `backend:[1]` 且 macOS 宿主能力表已声明(`PluginTypes.swift:493`),Windows 宿主对 backend 零引用。本 PR 已同步 preview,`fixtures/backend.plugin.json` 已随同步登记进覆盖清单。该插件属于本轮 M0–M10 冻结范围之外的新能力,这里只记录事实,不列为验收项。 +- 宿主能力与 Core 支持版本的事实来源:`macos/Sources/LitheModuleAPI/Plugins/PluginTypes.swift:492-504`(`PluginManifest.hostCapabilities`:backend:[1]、buildProviders:[1]、buildSystems:[1,2]、executableResolution:[1]、executionProviders:[1]、projectGenerators:[1]、serverProcess:[1]、settings:[1,2]、ui:[1,2,3]、vcsProviders:[1]、languages:[1]、languageServers:[1,2])与 `rust/lithe-core/src/plugins/capabilities.rs`(`SUPPORTED_CAPABILITIES`)。注意 `shared/contracts/plugins/v1/README.md` 能力一节仍写 macOS 不声明 buildProviders/vcsProviders(preview 上也保留),已落后于代码,以代码与功能矩阵为准(过时描述已记录在 fixture 覆盖审计 §5)。 ## 3. 分包类验收步骤(§1 通用链的裁剪点) diff --git a/docs/development/windows-plugin-fixture-coverage.md b/docs/development/windows-plugin-fixture-coverage.md index 68b037a56..65e1174d4 100644 --- a/docs/development/windows-plugin-fixture-coverage.md +++ b/docs/development/windows-plugin-fixture-coverage.md @@ -1,6 +1,6 @@ # Windows 插件 fixture 覆盖审计(M0) -状态:M0 交付。证据在基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a`(#1424 合入后的 preview 起点)上逐一核对,行号为该基线时点。 +状态:M0 交付。证据在基线 commit `1e78b59b6385fca6a881639b42048bf3bc587f4a`(#1424 合入后的 preview 起点)上逐一核对,并已同步 preview `83017afa`(插件后端能力 `8471b998`/#1470 合入后的契约状态,新增 `backend.plugin.json`);行号为对应时点。 本文回答路线图 [Windows 插件功能对齐计划](windows-plugin-parity-plan.md) M0 的要求:`shared/contracts/plugins/v1/fixtures` 的每个文件,是否有 Core 契约验证、是否有 Windows 产品宿主的**真实接线**测试,缺什么、归哪个阶段。机器可读清单是 @@ -11,10 +11,10 @@ ## 1. 结论摘要 -- fixtures 目录共 34 个文件:19 个合法清单(含原生 UI 3 个、项目生成 1 个、VCS 1 个,分类时单列)、1 个非法修改案例集、5 个金样/计划文件(command-plans、build-plans、runtime-versions、function-protocol、project-plans)、5 个市场用例、4 个项目生成模板。另有 `README.md` 与两个 schema 在契约根目录,不在 fixtures 内。 -- Core 侧:34 个文件全部有至少一个 Core 测试引用(模板目录经 `plan.rs` 的 `collect_files` 生产代码递归读取,被 golden 测试间接触发)。 -- Windows 侧:**3 个 wired**(有产品路径测试)、**10 个 validated-only**(仅清单验收/能力门控)、**10 个 indirect-core**(Windows 经 Core 命令消费、无直接读取)、**11 个 unwired**(Windows 零引用)。 -- 最严重的真实缺口:`build-tool-path`、`build-provider-language-server`、`native-panel`、`native-form`、`native-controls`、`project-generator`、`vcs-provider` 在 Windows 侧零引用;全部 5 个市场用例和 5 个跨平台金样在 Windows 没有直接消费测试。 +- fixtures 目录共 35 个文件:20 个合法清单(含原生 UI 3 个、项目生成 1 个、VCS 1 个,分类时单列;backend 为冻结路线图后新增)、1 个非法修改案例集、5 个金样/计划文件(command-plans、build-plans、runtime-versions、function-protocol、project-plans)、5 个市场用例、4 个项目生成模板。另有 `README.md` 与两个 schema 在契约根目录,不在 fixtures 内。 +- Core 侧:35 个文件全部有至少一个 Core 测试引用(模板目录经 `plan.rs` 的 `collect_files` 生产代码递归读取,被 golden 测试间接触发)。 +- Windows 侧:**3 个 wired**(有产品路径测试)、**10 个 validated-only**(仅清单验收/能力门控)、**10 个 indirect-core**(Windows 经 Core 命令消费、无直接读取)、**12 个 unwired**(Windows 零引用)。 +- 最严重的真实缺口:`build-tool-path`、`build-provider-language-server`、`native-panel`、`native-form`、`native-controls`、`project-generator`、`vcs-provider`、`backend` 在 Windows 侧零引用;全部 5 个市场用例和 5 个跨平台金样在 Windows 没有直接消费测试。 - 顺带发现(只记录,不修改,归属见 §5):`tests.rs:149` 的门控注释已过时;`lithe-plugin-docs` 的 `contract.lock.json` 落后于主仓库契约;macOS `BuildSystemToolWindowTests` 用本地 fixture 副本,共享 fixture 的改动不会反映到它。 ## 2. 检测工具与运行方式 @@ -50,11 +50,15 @@ node --test scripts/check-plugin-fixture-coverage.test.mjs | | indirect-core | Windows 通过 Core 命令消费其内容,宿主测试不直接读取该文件 | | | unwired | Windows 零直接引用(或仅作为 invalid-manifests 基底间接读取,单独说明) | -非 wired 的每条都带 `gapStage`(路线图里程碑 M0–M10)与 `gapDetail`。`gapStage` 表示**计划中该能力接入的阶段**,不是承诺日期;提前实现时以真实测试关闭并更新清单。 +非 wired 的每条都带 `gapStage` 与 `gapDetail`。`gapStage` 表示**计划中该能力接入的阶段**,不是承诺日期;取值为路线图里程碑 M0–M10,或冻结路线图之后落地的新能力用的 `followup`(按计划 §6 单列后续阶段,暂无编号)。提前实现时以真实测试关闭并更新清单。 ## 4. 分类明细 -### 4.1 合法清单(14 个 valid-manifest) +### 4.1 合法清单(15 个 valid-manifest;原生 UI、项目生成、VCS 三类单列见 §4.4) + +| Fixture | 结果 | Core 入口(节选) | Windows 入口 | Windows 状态 / 缺口 | +| --- | --- | --- | --- | --- | +| backend.plugin.json | success | `declarative_plugins.rs:32/615` 后端计划与宿主服务(8471b998) | 无 | **unwired** → followup:冻结路线图(M0–M10)之后的新能力,macOS 宿主已声明 `backend:[1]`,Windows 接入按计划 §6 单列后续阶段 | | Fixture | 结果 | Core 入口(节选) | Windows 入口 | Windows 状态 / 缺口 | | --- | --- | --- | --- | --- | @@ -77,11 +81,11 @@ node --test scripts/check-plugin-fixture-coverage.test.mjs | Fixture | 结果 | Core 入口 | Windows 入口 | Windows 状态 / 缺口 | | --- | --- | --- | --- | --- | -| invalid-manifests.json(154 用例,12 个基底) | rejection | `declarative_plugins.rs:130` | `tests.rs:80` 全量循环,宿主校验路径断言同码 | **wired**(校验路径一致性;宿主行为仍以各清单的 wired/validated-only 行为准) | +| invalid-manifests.json(163 用例,13 个基底,含 backend) | rejection | `declarative_plugins.rs:133` | `tests.rs:80` 全量循环,宿主校验路径断言同码 | **wired**(校验路径一致性;宿主行为仍以各清单的 wired/validated-only 行为准) | | command-plans.json | success | `declarative_plugins.rs:212` golden 展开 | 无;`tests.rs:433` 的 PHP 契约包端到端实测了 runPlan/testPlan 展开但不用本 golden | indirect-core → M5 | | build-plans.json | success | `build_systems.rs:143` | 无 | indirect-core → M4 | | runtime-versions.json | success | `declarative_plugins.rs:312` | 无(`launch.rs` 用 node/java/go 真实输出自测解析,不读本 golden) | indirect-core → M10:建议接入宿主探测路径时用本 fixture 对齐 | -| function-protocol.json | success | `plugin_function.rs:441` 起 7 组协议测试 | 无;网页桥 v0 对 `functions.*` 返回 `plugin.web.unknownMethod` | indirect-core → M6 | +| function-protocol.json | success | `plugin_function.rs:558` 起 8 组协议测试(v2 新增 hostRequests) | 无;网页桥 v0 对 `functions.*` 返回 `plugin.web.unknownMethod` | indirect-core → M6 | | project-plans.json | success | `project_generators.rs:65` | 无 | indirect-core → M8 | ### 4.3 市场用例(5 个 marketplace/) @@ -109,6 +113,7 @@ node --test scripts/check-plugin-fixture-coverage.test.mjs 3. **macOS 本地副本**:`macos/Tests/LitheTests/Fixtures/BuildSystems/` 下的 `build-command-output-file`、`build-host-integration` 等是共享 fixture 的本地副本,`BuildSystemToolWindowTests` 经 `Bundle.module` 加载副本。共享 fixture 语义变更不会自动反映到这批测试,属已知重复面,不在本轮收敛。 4. **非法清单循环的边界**:Windows `invalid-manifests` 循环(154 用例)与 macOS 同名测试是契约一致性回归;它们不能替代宿主行为测试,也不能作为"Windows 已支持某能力"的证据——被门控清单(如 build 组)的全量拒绝恰恰说明能力未开放。 5. **fixtur 目录枚举现状**:Core 测试全部用 `include_str!`/显式 const 清单,Windows 测试用 `read_fixture(name)` 显式点名;唯一对 fixtures 树的运行时递归读取是 `plan.rs` 的模板收集。这就是"新增 fixture 会静默漏掉"的结构性原因,也是本检测器存在的理由。`scripts/verify-shared-contracts.sh` 的语义校验(schemaVersion、ID 前缀、拒绝码文档化等)与本检测器互补,不互相替代。 +6. **过时的宿主能力描述**:`shared/contracts/plugins/v1/README.md` 能力一节(preview 上仍在 §456 附近)称 macOS 不声明 buildProviders/vcsProviders;基线与 preview 的 `PluginTypes.swift` `PluginManifest.hostCapabilities`(preview 为 `:492-504`)实际声明 `buildProviders:[1]、buildSystems:[1,2]、executableResolution:[1]、executionProviders:[1]、projectGenerators:[1]、ui:[1,2,3]、vcsProviders:[1]`(preview 另加 `backend:[1]`),Core `capabilities.rs` 的 `SUPPORTED_CAPABILITIES` 支持同一组(含 `buildSystems:2`)。功能矩阵佐证:`build-provider-refresh-and-tool` 与 `cargo-build-plugin` 记 macOS implemented/验证 pending。README 落后于代码,能力事实以 Core 与功能矩阵为准,不能以旧 README 推断安装可用性。 ## 6. 维护规则 @@ -116,4 +121,5 @@ node --test scripts/check-plugin-fixture-coverage.test.mjs - 删除/重命名 fixture:同 PR 清理清单,否则 `missing-fixture` 失败。 - 移动/删除被引用的测试文件:更新证据条目,否则 `evidence-path` 失败。 - 关闭 gap:以真实 Windows 测试为证据更新 `windowsStatus`/`windowsEvidence` 并清空 gap 字段;检测器会拒绝"没有证据的 wired"。 +- M1 交接(#1476,截至 2026-10-25 仍为 OPEN Draft):其 `fixtures/signature-vectors.json` 未登记时,Linux 门禁会以 `unknown-fixture` 让该 PR 失败——这是门禁设计行为。`signature-vectors` 类别已在检测器预置,且是该类别唯一允许 `coreEvidence` 为空的例外(签名向量由双端宿主签名测试消费,无 Core 校验面;其他类别仍强制 Core 证据)。#1476 合入 preview 后按最小补丁登记:`category: "signature-vectors"`、`coreEvidence: []`、Windows 证据 `windows/tauri/src-tauri/src/plugin_host/signature.rs:663 shared_fixture_verifies_end_to_end`(macOS 侧对照为 `PluginSignatureVectorTests.swift` 四组 @Test),`windowsStatus` 按其 PR 实际证据标注。`backend.plugin.json` 已由本 PR 随 preview 同步登记,M1 worktree 的组合检测现在只剩 `signature-vectors.json` 一个 unknown。合并顺序优先 #1474 先合;若 #1476 先合,它需要先带上上述登记条目才能通过门禁。 - 本文是审计快照;实时约束以清单 + 检测器为准。行号漂移不需要修本文,但证据文件消失必须处理。 diff --git a/scripts/check-plugin-fixture-coverage.mjs b/scripts/check-plugin-fixture-coverage.mjs index 941ed255b..be25fb835 100644 --- a/scripts/check-plugin-fixture-coverage.mjs +++ b/scripts/check-plugin-fixture-coverage.mjs @@ -34,6 +34,11 @@ export const CATEGORY_VALUES = [ 'project-plans', 'project-generation', 'marketplace', + // Shared acceptance data for publisher-signature verification (M1); the + // fixture lands with PR #1476 and is registered once it reaches preview. + // Both hosts consume it in their own signature tests, so there is no Core + // validation surface and coreEvidence is optional for this category. + 'signature-vectors', 'native-ui', 'vcs-provider', 'template-assets', @@ -43,7 +48,10 @@ export const OUTCOME_VALUES = ['success', 'rejection', 'partial-support']; export const WINDOWS_STATUS_VALUES = ['wired', 'validated-only', 'indirect-core', 'unwired']; -const GAP_STAGE_PATTERN = /^M(10|[1-9])$/; +// Roadmap stages M0-M10, plus `followup` for capabilities that landed after +// the roadmap froze: the plan routes those into separately numbered later +// stages (section 6) that do not exist yet, so no M-number may be invented. +export const GAP_STAGE_PATTERN = /^(M(10|[0-9])|followup)$/; export function defaultRepoRoot() { return join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -196,8 +204,8 @@ function validateEntry(entry, repoRoot, push) { if (!WINDOWS_STATUS_VALUES.includes(entry.windowsStatus)) { push('invalid-entry', `${path ?? ''}: unknown windowsStatus ${JSON.stringify(entry.windowsStatus)}`); } - checkEvidence(entry.path, entry.coreEvidence, 'coreEvidence', repoRoot, push); - checkEvidence(entry.path, entry.windowsEvidence, 'windowsEvidence', repoRoot, push); + checkEvidence(entry.path, entry.coreEvidence, 'coreEvidence', repoRoot, push, entry.category !== 'signature-vectors'); + checkEvidence(entry.path, entry.windowsEvidence, 'windowsEvidence', repoRoot, push, true); const wired = entry.windowsStatus === 'wired'; if (wired) { @@ -220,16 +228,16 @@ function validateEntry(entry, repoRoot, push) { } } -function checkEvidence(path, evidence, field, repoRoot, push) { +function checkEvidence(path, evidence, field, repoRoot, push, required) { if (evidence === undefined) { - if (field === 'coreEvidence') push('invalid-entry', `${path ?? ''}: coreEvidence is required`); + if (required) push('invalid-entry', `${path ?? ''}: ${field} is required`); return; } if (!Array.isArray(evidence)) { push('invalid-entry', `${path ?? ''}: ${field} must be an array of strings`); return; } - if (field === 'coreEvidence' && evidence.length === 0) { + if (required && field === 'coreEvidence' && evidence.length === 0) { push('invalid-entry', `${path}: coreEvidence must name at least one Core-side reference`); return; } diff --git a/scripts/check-plugin-fixture-coverage.test.mjs b/scripts/check-plugin-fixture-coverage.test.mjs index 60b5e3d8c..d0804ae14 100644 --- a/scripts/check-plugin-fixture-coverage.test.mjs +++ b/scripts/check-plugin-fixture-coverage.test.mjs @@ -9,6 +9,7 @@ import { defaultRepoRoot, enumerateFixtureFiles, evidenceFilePath, + GAP_STAGE_PATTERN, runCoverageCheck, } from './check-plugin-fixture-coverage.mjs'; @@ -29,6 +30,10 @@ function tempRepo(t) { mkdirSync(dirname(join(fixturesDir, ...relativePath.split('/'))), { recursive: true }); writeFileSync(join(fixturesDir, ...relativePath.split('/')), '{}\n'); }, + writeFile(relativePath, text) { + mkdirSync(dirname(join(root, ...relativePath.split('/'))), { recursive: true }); + writeFileSync(join(root, ...relativePath.split('/')), text); + }, writeManifest(entries, extra = {}) { const manifestPath = join(root, 'scripts', 'plugin-fixture-coverage.json'); mkdirSync(dirname(manifestPath), { recursive: true }); @@ -239,9 +244,10 @@ test('evidence parsing keeps the file part and drops the line number', () => { }); // The enforcement test: the real manifest must match the real fixtures -// directory right now, and its known gaps must stay explicit. When this test -// fails, classify the new fixture or clean the stale entry - never weaken -// this assertion to make a change pass. +// directory right now, and every non-wired entry must keep an honest gap +// classification. Completeness is never asserted here - the day every +// fixture is genuinely wired on Windows this test must still pass; the +// all-wired positive case below proves that path exists. test('the real fixture directory and classification list agree', () => { const repoRoot = defaultRepoRoot(); const manifest = JSON.parse(readFileSync(defaultManifestPath(), 'utf8')); @@ -249,11 +255,105 @@ test('the real fixture directory and classification list agree', () => { const result = runCoverageCheck({ repoRoot, fixturesDir, manifest }); assert.equal(result.ok, true, result.problems.map((problem) => `${problem.kind}: ${problem.detail}`).join('\n')); assert.ok(result.summary.fixtures >= 30, `expected the full fixture set, got ${result.summary.fixtures}`); - assert.ok(result.summary.knownGaps > 0, 'the audit documents real gaps; a zero-gap manifest means the statuses were flattened'); for (const fixture of manifest.fixtures) { if (fixture.windowsStatus !== 'wired') { - assert.match(fixture.gapStage, /^M(10|[1-9])$/, `${fixture.path} gap stage`); + assert.match(fixture.gapStage, GAP_STAGE_PATTERN, `${fixture.path} gap stage`); assert.ok(fixture.gapDetail.trim().length > 0, `${fixture.path} gap detail`); } } }); + +// The anti-forgery stance is honesty, not permanent partiality: a manifest +// that wires every fixture with real evidence must pass with zero known +// gaps, so the gate can never be satisfied by keeping statuses partial. +test('a fully wired manifest with valid evidence passes with zero known gaps', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeFixture('b.json'); + repo.writeManifest([entry('a.json'), entry('b.json')]); + const result = repo.check(); + assert.equal(result.ok, true, result.problems.map((problem) => `${problem.kind}: ${problem.detail}`).join('\n')); + assert.equal(result.summary.knownGaps, 0); + assert.equal(result.summary.wired, 2); +}); + +test('M0 and followup are legal gap stages, signature-vectors a legal category, M11 is not', (t) => { + const repo = tempRepo(t); + repo.writeFixture('a.json'); + repo.writeFixture('b.json'); + repo.writeFixture('c.json'); + repo.writeManifest([ + entry('a.json', { + category: 'signature-vectors', + windowsStatus: 'validated-only', + gapStage: 'M0', + gapDetail: 'baseline problem owned by an M0 focused fix PR', + windowsEvidence: [], + }), + entry('b.json', { windowsStatus: 'unwired', gapStage: 'M0', gapDetail: 'also owned by M0', windowsEvidence: [] }), + entry('c.json', { + windowsStatus: 'unwired', + gapStage: 'followup', + gapDetail: 'capability landed after the roadmap froze; awaiting its numbered stage', + windowsEvidence: [], + }), + ]); + const result = repo.check(); + assert.equal(result.ok, true, result.problems.map((problem) => `${problem.kind}: ${problem.detail}`).join('\n')); + assert.equal(result.summary.gapStages.M0, 2); + assert.equal(result.summary.gapStages.followup, 1); + + repo.writeManifest([ + entry('a.json', { windowsStatus: 'unwired', gapStage: 'M11', gapDetail: 'beyond the roadmap', windowsEvidence: [] }), + ]); + const rejected = repo.check(); + assert.equal(rejected.ok, false); + assert.ok(rejected.problems.some((problem) => /must name a roadmap stage/.test(problem.detail))); +}); + +// Signature vectors are shared acceptance data consumed by each host's own +// signature tests; there is no Core validation surface, so coreEvidence may +// be empty as long as the Windows side carries real evidence. +test('signature-vectors entries may carry host-side evidence only', (t) => { + const repo = tempRepo(t); + repo.writeFixture('signature-vectors.json'); + // The Windows signature test file the evidence points at must exist in the + // throwaway repository, mirroring the real M1 tree. + repo.writeFile('windows/tauri/src-tauri/src/plugin_host/signature.rs', 'mod tests {}'); + repo.writeManifest([ + entry('signature-vectors.json', { + category: 'signature-vectors', + outcome: 'success', + coreEvidence: [], + windowsEvidence: ['windows/tauri/src-tauri/src/plugin_host/signature.rs:663 shared_fixture_verifies_end_to_end'], + }), + ]); + const result = repo.check(); + assert.equal(result.ok, true, result.problems.map((problem) => `${problem.kind}: ${problem.detail}`).join('\n')); + + // The exemption must not become a hole: other categories still require + // Core-side evidence even when Windows evidence is present. + repo.writeFixture('plain.json'); + repo.writeManifest([ + entry('signature-vectors.json', { + category: 'signature-vectors', + coreEvidence: [], + windowsEvidence: ['windows/tauri/src-tauri/src/plugin_host/signature.rs:663 shared_fixture_verifies_end_to_end'], + }), + entry('plain.json', { coreEvidence: [], windowsEvidence: [fixtureEvidence('plain.json')] }), + ]); + const rejected = repo.check(); + assert.equal(rejected.ok, false); + assert.ok(rejected.problems.some((problem) => /plain\.json: coreEvidence must name/.test(problem.detail))); +}); + +// CI wiring guard: the Linux changes lane must invoke both the detector CLI +// and this test file. If the workflow stops calling them, a new fixture can +// land unregistered without any real pull request check failing. +test('the Linux changes lane wires the detector and its tests into CI', () => { + const workflow = readFileSync(join(defaultRepoRoot(), '.github', 'workflows', 'ci-macos.yml'), 'utf8'); + assert.ok(workflow.includes('run: node scripts/check-plugin-fixture-coverage.mjs'), 'detector CLI gate step missing'); + assert.ok(workflow.includes('scripts/check-plugin-fixture-coverage.test.mjs'), 'test invocation missing'); + assert.ok(workflow.includes('--test-timeout=15000'), 'per-test deadline missing'); + assert.ok(workflow.includes('plugin-fixture-coverage.junit.xml'), 'junit timing evidence missing'); +}); diff --git a/scripts/plugin-fixture-coverage.json b/scripts/plugin-fixture-coverage.json index 6971c008e..0ef0c411a 100644 --- a/scripts/plugin-fixture-coverage.json +++ b/scripts/plugin-fixture-coverage.json @@ -1,8 +1,21 @@ { "schemaVersion": 1, - "description": "Explicit classification of every file under shared/contracts/plugins/v1/fixtures. Checked by scripts/check-plugin-fixture-coverage.mjs: the directory enumeration and this list must match exactly. windowsStatus: wired = a product-path Windows test exercises this fixture; validated-only = only manifest validation/gating; indirect-core = Windows consumes it through Core commands without reading the file; unwired = no Windows list or test names it. A non-wired status is a known gap and must keep gapStage/gapDetail until a real test closes it. Evidence strings are '[:] '; the checker verifies the file exists, lines are documentation as of base commit 1e78b59b6385fca6a881639b42048bf3bc587f4a.", + "description": "Explicit classification of every file under shared/contracts/plugins/v1/fixtures. Checked by scripts/check-plugin-fixture-coverage.mjs (wired into the ci-macos.yml changes job on Linux): the directory enumeration and this list must match exactly. windowsStatus: wired = a product-path Windows test exercises this fixture; validated-only = only manifest validation/gating; indirect-core = Windows consumes it through Core commands without reading the file; unwired = no Windows list or test names it. A non-wired status is a known gap and must keep gapStage/gapDetail until a real test closes it; gapStage is a roadmap stage M0-M10 or 'followup' for capabilities that landed after the roadmap froze. Evidence strings are '[:] '; the checker verifies the file exists, lines are documentation as of base commit 1e78b59b6385fca6a881639b42048bf3bc587f4a synced with preview 83017afa851c83f119f40268bf9eb04b26aefc91 (backend capability).", "fixturesDirectory": "shared/contracts/plugins/v1/fixtures", "fixtures": [ + { + "path": "backend.plugin.json", + "category": "valid-manifest", + "outcome": "success", + "coreEvidence": [ + "rust/lithe-core/src/tests/declarative_plugins.rs:32 BACKEND_FIXTURE", + "rust/lithe-core/src/tests/declarative_plugins.rs:615 plugin_backend_is_planned_without_a_workspace_and_names_its_host_services" + ], + "windowsEvidence": [], + "windowsStatus": "unwired", + "gapStage": "followup", + "gapDetail": "Windows 侧零引用。插件后端(backend:1)与函数协议 v2 是冻结路线图(2026-10-24 版 M0–M10)之后的新能力(preview 8471b998/#1470),macOS 宿主已声明 backend:[1](PluginTypes.swift:493);Windows 接入按计划 §6 单列后续阶段,暂无编号。" + }, { "path": "build-command-output-file.plugin.json", "category": "valid-manifest", @@ -161,8 +174,9 @@ "category": "function-protocol", "outcome": "success", "coreEvidence": [ - "rust/lithe-core/src/lsp/interface/plugin_function.rs:441 the_offer_matches_the_fixture", - "rust/lithe-core/src/lsp/interface/plugin_function.rs:528 outcomes_follow_the_fixture" + "rust/lithe-core/src/lsp/interface/plugin_function.rs:558 the_offer_matches_the_fixture", + "rust/lithe-core/src/lsp/interface/plugin_function.rs:648 outcomes_follow_the_fixture", + "rust/lithe-core/src/lsp/interface/plugin_function.rs:703 host_requests_follow_the_fixture (protocol v2)" ], "windowsEvidence": [], "windowsStatus": "indirect-core", @@ -499,7 +513,7 @@ "windowsEvidence": [], "windowsStatus": "unwired", "gapStage": "M9", - "gapDetail": "Windows 侧零引用;VCS Provider 会话、根识别与宿主入口未接入(macOS 也未声明 vcsProviders)。" + "gapDetail": "Windows 侧零引用;VCS Provider 会话、根识别与宿主入口未接入。macOS 宿主已声明 vcsProviders:[1](PluginTypes.swift:502)且矩阵 vcs-subversion-and-plugin-providers 记 macOS partial/验证 pending(VcsProviderHostTests 覆盖根识别、监听与生命周期自动化);对齐 macOS 已有子能力属 M9。" }, { "path": "web-panel.plugin.json",