mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(refactor): [BASE-02] freeze real-browser public API contracts
This commit is contained in:
1 parent
833cc11674
commit
b382ec08f7
12 files changed
+4034
-22
No files matched your search
@@ -28,3 +28,7 @@ BASE-01 于 2026-09-10 固定核心与 chapter 的 npm 发布内容,数据在
|
||||
脚本不安装或执行发布包,不修改生成产物,不把整个 tarball 解压到工作区。归档成员通过 tar 读到内存;后续浏览器服务可复用 ensureArchive/readMember。仅缓存写入,JSON 与说明为持久来源记录;没有新增生产依赖。
|
||||
|
||||
修改冻结基线必须通过独立任务和变更记录;不能直接覆盖哈希来消除回归。真实样本或报告一旦生成,绑定包版本/完整性和测试 ID。
|
||||
|
||||
## 公共 API 报告
|
||||
|
||||
BASE-02 已在内置浏览器取得核心/chapter 的同步 API 和描述符快照,见 [覆盖与重跑](api-coverage.md) 和 [冻结报告](public-api.json)。事件/生命周期、DOM、类型、分发和其他包仍由各自任务补齐。
|
||||
@@ -0,0 +1,47 @@
|
||||
# 公共 API 基线与契约覆盖
|
||||
|
||||
BASE-02 使用固定 npm artplayer 5.4.0 + artplayer-plugin-chapter 1.1.0 的 UMD 文件,在 Codex 内置浏览器执行。UA 报告 Windows / Chrome 152.0.0.0,语言 zh-CN;这不是外部 Chrome 扩展或其他浏览器已验证的声明。
|
||||
|
||||
## 已保存的事实
|
||||
|
||||
[public-api.json](public-api.json) 包含原始报告、已加载脚本路径、发布包版本/integrity/成员哈希、夹具源码指纹、真实采集时间和重复采集校验结果。最终两次独立 reload 的 14 项检查全部通过,快照相同,浏览器 error/warn 日志为空。
|
||||
|
||||
| 快照 | 已观察范围 |
|
||||
| --- | --- |
|
||||
| static/constants | 47 个静态属性描述符,33 个公开大写配置默认值 |
|
||||
| defaults/config | 50 个默认选项、config 配置形状;函数用标记保存,不比较函数源码 |
|
||||
| prototype/emitterPrototype | 核心原型 6 项、Emitter 原型 5 项 |
|
||||
| instance/instanceResolved | 实例自身 74 项、沿原型查找可访问的 84 项属性描述符 |
|
||||
| utilityDescriptors | 48 个工具属性的类型和描述符,不代表已测试全部工具行为 |
|
||||
| integrations | template/events/controls/setting/contextmenu/layers/plugins/storage/subtitle/i18n 共 10 个子系统的 own、直接 prototype 和完整属性查找结果 |
|
||||
| chapterFactory/chapterResult | 工厂和插件返回对象形状,保持同步注册及结果访问 |
|
||||
|
||||
沿原型查找排除 Object.prototype,保留最近定义;这样能发现 controls.show/toggle 等继承 API 的缺失。快照也会包含实现细节,发现差异时需要区分公开契约和可调整内部状态,记录依据及消费者测试;不能自动覆盖旧快照消除失败,也不能把每个内部反射差异一律当作禁止重构。
|
||||
|
||||
语言默认值先实测等于 navigator.language.toLowerCase(),再用环境引用标记比较,实际语言保留在报告中。其他环境差异不自动忽略,例如 Safari preload 默认值需在其独立基线中核对。STYLE 大字符串不作默认值精确比较;CSS/DOM 契约由 BASE-04 负责。夹具指纹使用 SHA-256 和 LF 规范化,避免 Git 换行转换影响跨系统复跑;发布 JS 使用原始字节哈希。
|
||||
|
||||
## 固定测试 ID 和后续责任
|
||||
|
||||
所有已执行条目都绑定上述两个发布版本及 public-api.json;入口为 ../fixtures/api.html,原断言在 ../fixtures/api.js,比较命令见下节。
|
||||
|
||||
| 契约 | 已执行的固定 ID / 快照路径 | 未覆盖部分与责任 |
|
||||
| --- | --- | --- |
|
||||
| API-01 | API-01.version、defaults-independent、default-language、instance-registration、container-selector;snapshot.defaults/constants/config | ready 回调、useSSR、失败构造、合并边界由 BASE-03/CORE-01/CORE-07 接续 |
|
||||
| API-02 | API-02.video、bound-query;snapshot.instanceResolved | 播放/切源 Promise、参数边界及其他可抽取方法由 BASE-03 和对应 CORE 任务接续 |
|
||||
| API-03 | api.test.mjs 的 API comparison 用例;static/prototype/emitterPrototype/instance/integrations 描述符 | 比较器负例验证 writable/接口删除/默认值/继承访问变化;其他构造模式由相关场景补充 |
|
||||
| API-04 | API-04.on-return、off | once/ctx、重入、异常、事件顺序由 BASE-03 接续 |
|
||||
| API-05 | API-05.destroy-return、instance-cleanup | 本轮是单实例首次 destroy(true);重复销毁、多实例、removeHtml=false 和资源/媒体顺序由 BASE-03 接续 |
|
||||
| API-06 | API-06.sync-chapter、add-return、result-call;chapterFactory/chapterResult | 异步注册、重名及 chapter 参数和 UI 由 BASE-03、PKG-CHAPTER-01/02 接续 |
|
||||
| API-07 | snapshot.integrations 及 utilityDescriptors | 描述符覆盖不等于 UI/轨道/资源行为通过;BASE-04、插件试点和对应 CORE 任务接续 |
|
||||
| API-08/09/10/11/12 | 此任务不声明完整覆盖 | BASE-04/05/06/08 与对应包任务负责 DOM/CSS、分发、浏览器、类型、持久协议 |
|
||||
|
||||
表中的同前缀短名均指相同契约前缀的完整 ID,例如 defaults-independent 即 API-01.defaults-independent。执行断言列表以 api.mjs 的 checkIds 和报告为准。ENG-09 后续将覆盖索引纳入机器检查;这里没有将整条 API-01 等宽泛契约标成全部通过。
|
||||
|
||||
## 重跑和比较
|
||||
|
||||
1. 运行 `node refactor/scripts/browser-server.mjs`,按需要设置 ARTPLAYER_TEST_PORT,默认 8083。
|
||||
2. 使用 @Chrome;不可用时直接使用内置浏览器,打开 http://127.0.0.1:8083/fixtures/api.html。每次明确 reload,看到 PASS 且 errors 为空;此次页面只验证同步 API,不等待或验证视频播放。
|
||||
3. 运行 `node refactor/scripts/api.mjs --compare refactor/.cache/reports/api.json`。检查全部固定 ID、发布来源、实际脚本、夹具指纹,再比较快照。命令不改冻结文件。
|
||||
4. `node refactor/scripts/api.mjs --check` 只核对持久报告和来源/快照摘要;`node --test refactor/scripts/api.test.mjs` 运行报告/比较器负例。两者不启动浏览器,CI 不能把它们当作新一轮真实浏览器执行。
|
||||
|
||||
yarn test:baseline / ci:check 已通过通配入口包含新增 api.test.mjs。负例涵盖缺检查、错误来源、采集失败、描述符变化、删除方法、选项/静态默认值变化和继承 API 删除。正式候选对照和浏览器自动回归由 ENG-05/07 逐步接入,当前夹具仍固定旧发布文件,不自动加载工作区候选。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
# BASE-02:冻结公共 API 与默认值
|
||||
|
||||
日期:2026-09-10;分支 codex/compatible-modernization;起点 833cc116。
|
||||
|
||||
## 交付
|
||||
|
||||
扩充 BASE-HARNESS-01 的真实发布包夹具,保存核心 5.4.0 / chapter 1.1.0 的公共 API 描述符和默认值。加入静态配置、继承属性查找、绑定 query、构造选项身份、插件同步返回和销毁返回断言。服务器给报告附加其提供的发布成员和夹具来源指纹。
|
||||
|
||||
内置浏览器最终两次 reload 的 14 项检查全部通过,快照完全相同,error/warn 日志为空;UA 为 Windows / Chrome 152.0.0.0,语言 zh-CN。报告见 [public-api.json](../baselines/public-api.json),细分的契约、固定测试 ID、重跑命令、限制和后续责任见 [api-coverage.md](../baselines/api-coverage.md)。
|
||||
|
||||
新增 api.mjs 检查持久报告或比较新采集报告,明确区分“校验已保存的报告”和“重新执行浏览器”。新增两项 Node 测试验证报告必须完整且来源匹配,以及描述符/方法/默认值/继承接口回归会被拒绝。不提供自动覆盖冻结快照的命令。
|
||||
|
||||
## 兼容与验证
|
||||
|
||||
没有修改生产源码、公开声明、依赖或发布产物。固定包依旧由归档完整性与成员哈希校验后提供。语言默认值按真实环境关系验证后规范化;其他差异不自动忽略。API 比较记录可观察形状,反射中的内部字段变化仍需要兼容评估,不是把原内部架构永久锁死。
|
||||
|
||||
- 14 个浏览器断言、两次最终快照一致性通过。
|
||||
- 新增 2 个报告/比较器测试及原归档/HTTP 基线测试通过,已由 test:baseline 纳入 CI。
|
||||
- 夹具/比较器语法、计划/链接、Git 差异与完整 ci:check 验证通过。
|
||||
- 不宣称真实播放、完整生命周期、全部插件、类型或候选新旧组合通过;对应任务继续待办。
|
||||
|
||||
## 接续与回退
|
||||
|
||||
每项契约缺口与下一任务记录在 api-coverage.md。下一步 BASE-03 验证事件和生命周期;Chrome 不可用时继续使用内置浏览器。工作区/发布基线差异、第三方 SDK、真实设备和发布门槛保持。
|
||||
|
||||
本任务独立本地提交,主题包含 BASE-02。可撤销该提交恢复旧夹具;冻结数据保留于 Git 历史,不能直接更新快照隐藏后续回归。没有推送或发布。
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
`node refactor/scripts/browser-server.mjs` 在 127.0.0.1:8083 启动独立测试服务;ARTPLAYER_TEST_PORT 可更换端口。它不启动或修改用户的 Chrome,也不占用 docs 的 8082 开发服务。
|
||||
|
||||
- `/fixtures/api.html` 加载固定核心/chapter tarball 的真实 JS,准备捕获描述符、默认配置、utils/子系统/插件形状及基础调用。
|
||||
- `/fixtures/api.html` 加载固定核心/chapter tarball 的真实 JS,捕获描述符、默认配置、utils/子系统/插件形状及基础调用。
|
||||
- `/releases/<name>/<member>` 只读取 releases.json 明确登记的成员;启动时核对 archive 完整性,不使用未编译源码或 CDN latest。
|
||||
- `/assets/sample/` 读取本仓库样本,支持普通 bytes 范围请求。当前服务用于最小基线;不是完整 ENG-05 故障注入或 HTTP Range 兼容实现,不支持后缀/多段 Range。
|
||||
- 页面将 `public-api` 报告发送给同源 `/reports/api`,服务器保存 refactor/.cache/reports/api.json。最多 1 MiB;其他 Origin 和未知写入路径失败。临时报告不能自动覆盖冻结快照。
|
||||
|
||||
运行 `node --test refactor/scripts/browser-server.test.mjs` 验证真实 HTTP 服务返回原始 JS 字节、样本范围和路由隔离;`node --check refactor/fixtures/api.js` 只检查脚本语法。两者不能证明页面能在浏览器运行。
|
||||
|
||||
2026-09-10:Chrome 连接服务失败后,用户明确授权使用 Codex 内置浏览器。内置浏览器已打开 api.html,9 项基础 API 检查全部通过,页面 errors 为空,报告已保存至缓存。BASE-02 连接阻塞解除,接续核对报告和来源、生成持久基线;此时尚未完成整个 API 基线任务,也未验证真实播放。
|
||||
2026-09-10:按用户授权使用内置浏览器后,BASE-02 最终两次采集的 14 项检查均通过,快照相同,errors 为空。持久报告、比较命令、契约覆盖及剩余范围见 [API 基线](../baselines/api-coverage.md)。本页面没有验证真实播放。
|
||||
|
||||
夹具与报告路径由基线任务维护;ENG-05 可复用并扩充受控等待、媒体错误和正式自动化。api.js 依据已核对源码设计断言,真实发布行为如有不同应调查并记录,不能修改断言来盲目通过。
|
||||
+45
-15
@@ -1,9 +1,14 @@
|
||||
/* global Artplayer, artplayerPluginChapter */
|
||||
(async () => {
|
||||
const report = { kind: 'public-api', environment: { userAgent: navigator.userAgent }, checks: [], errors: [] }
|
||||
const check = (name, result) => {
|
||||
report.checks.push({ name, passed: Boolean(result) })
|
||||
if (!result) throw new Error(name)
|
||||
const report = {
|
||||
kind: 'public-api',
|
||||
environment: { userAgent: navigator.userAgent, language: navigator.language },
|
||||
scripts: [...document.scripts].map(script => new URL(script.src).pathname),
|
||||
checks: [], errors: [],
|
||||
}
|
||||
const check = (id, result) => {
|
||||
report.checks.push({ id, passed: Boolean(result) })
|
||||
if (!result) throw new Error(id)
|
||||
}
|
||||
function descriptors(object) {
|
||||
return Object.fromEntries(Object.getOwnPropertyNames(object).sort().map((name) => {
|
||||
@@ -23,37 +28,57 @@
|
||||
if (input && typeof input === 'object') return Object.fromEntries(Object.keys(input).sort().map(key => [key, valueOf(input[key])]))
|
||||
return input
|
||||
}
|
||||
function resolvedDescriptors(object) {
|
||||
const result = new Map()
|
||||
for (let current = object; current && current !== Object.prototype; current = Object.getPrototypeOf(current)) {
|
||||
for (const [name, descriptor] of Object.entries(descriptors(current))) {
|
||||
if (!result.has(name)) result.set(name, descriptor)
|
||||
}
|
||||
}
|
||||
return Object.fromEntries([...result].sort(([a], [b]) => a.localeCompare(b)))
|
||||
}
|
||||
let art
|
||||
try {
|
||||
check('published core version', Artplayer.version === '5.4.0')
|
||||
check('API-01.version', Artplayer.version === '5.4.0')
|
||||
const defaults = valueOf(Artplayer.option)
|
||||
check('API-01.defaults-independent', Artplayer.option !== Artplayer.option && Artplayer.option.subtitle !== Artplayer.option.subtitle)
|
||||
check('API-01.default-language', defaults.lang === navigator.language.toLowerCase())
|
||||
defaults.lang = { $environment: 'navigator.language.toLowerCase()' }
|
||||
const constants = Object.fromEntries(Object.getOwnPropertyNames(Artplayer).sort()
|
||||
.filter(name => /^[A-Z_]+$/.test(name) && name !== 'STYLE')
|
||||
.map(name => [name, valueOf(Artplayer[name])]))
|
||||
art = new Artplayer({ container: '.player', url: '/assets/sample/video.mp4', muted: true, plugins: [artplayerPluginChapter()] })
|
||||
check('constructor registers exact instance', Artplayer.instances.includes(art))
|
||||
check('native media element exposed', art.video instanceof HTMLVideoElement)
|
||||
check('chapter registered synchronously', art.plugins.artplayerPluginChapter?.name === 'artplayerPluginChapter')
|
||||
check('API-01.instance-registration', Artplayer.instances.includes(art))
|
||||
check('API-01.container-selector', art.template.$container === document.querySelector('.player'))
|
||||
check('API-02.video', art.video instanceof HTMLVideoElement && art.video === art.template.$video)
|
||||
const query = art.query
|
||||
check('API-02.bound-query', query('.art-video') === art.video)
|
||||
check('API-06.sync-chapter', art.plugins.artplayerPluginChapter?.name === 'artplayerPluginChapter')
|
||||
const plugin = { name: 'baselineProbe', ping: () => 'pong' }
|
||||
const added = art.plugins.add(() => plugin)
|
||||
check('plugins.add returns synchronous registry', added === art.plugins)
|
||||
check('plugin result methods callable', art.plugins.baselineProbe.ping() === 'pong')
|
||||
check('API-06.add-return', added === art.plugins)
|
||||
check('API-06.result-call', art.plugins.baselineProbe.ping() === 'pong')
|
||||
let received = 0
|
||||
const handler = amount => { received += amount }
|
||||
check('on is chainable', art.on('baseline:event', handler) === art)
|
||||
check('API-04.on-return', art.on('baseline:event', handler) === art)
|
||||
art.emit('baseline:event', 3)
|
||||
art.off('baseline:event', handler)
|
||||
art.emit('baseline:event', 7)
|
||||
check('custom event on/emit/off', received === 3)
|
||||
check('API-04.off', received === 3)
|
||||
const integrations = {}
|
||||
for (const name of ['template', 'events', 'controls', 'setting', 'contextmenu', 'layers', 'plugins', 'storage', 'subtitle', 'i18n']) {
|
||||
integrations[name] = { own: descriptors(art[name]), prototype: descriptors(Object.getPrototypeOf(art[name])) }
|
||||
integrations[name] = { own: descriptors(art[name]), prototype: descriptors(Object.getPrototypeOf(art[name])), resolved: resolvedDescriptors(art[name]) }
|
||||
}
|
||||
report.snapshot = {
|
||||
static: descriptors(Artplayer),
|
||||
prototype: descriptors(Artplayer.prototype),
|
||||
emitterPrototype: descriptors(Object.getPrototypeOf(Artplayer.prototype)),
|
||||
defaults,
|
||||
constants,
|
||||
config: valueOf(Artplayer.config),
|
||||
utilityDescriptors: descriptors(Artplayer.utils),
|
||||
instance: descriptors(art),
|
||||
instanceResolved: resolvedDescriptors(art),
|
||||
chapterFactory: descriptors(artplayerPluginChapter),
|
||||
chapterResult: descriptors(art.plugins.artplayerPluginChapter),
|
||||
integrations,
|
||||
@@ -64,8 +89,13 @@
|
||||
}
|
||||
finally {
|
||||
if (art) {
|
||||
art.destroy(true)
|
||||
report.checks.push({ name: 'instance cleanup', passed: !Artplayer.instances.includes(art) })
|
||||
try {
|
||||
check('API-05.destroy-return', art.destroy(true) === undefined)
|
||||
check('API-05.instance-cleanup', !Artplayer.instances.includes(art))
|
||||
}
|
||||
catch (error) {
|
||||
report.errors.push(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
const response = await fetch('/reports/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(report) })
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 196 / doing 0 / blocked 0 / done 18 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 195 / doing 0 / blocked 0 / done 19 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| BASE-01 | workspace<br>固定核心与试点的最小发布基线 | DOC-07 | 核心及 chapter 的 npm tarball/integrity、源码 SHA、已知支持窗口;其他包标待核对 | 核心/试点来源可重跑;不虚构全生态已验证,各包 01 步负责补齐自身历史发布基线 | H | done |
|
||||
| BASE-HARNESS-01 | workspace<br>建立最小发布包浏览器采集夹具与服务 | BASE-01 | 固定 tarball 静态服务、本地媒体 Range、公开 API 采集页面及受控报告路径 | HTTP 精确内容/范围和路由隔离测试、夹具语法通过;浏览器采集仍由 BASE-02 负责 | M | done |
|
||||
| BASE-02 | workspace<br>捕获公共 API 和属性描述符 | BASE-01, BASE-HARNESS-01 | 核心与试点的构造/默认值/静态和实例 API 快照,其余包由 01/02 步补齐;登记契约/版本/固定测试 ID/命令/报告与负责任务 | 真实发布包反射与旧用户调用可重跑,未验证项明确 | H | todo |
|
||||
| BASE-02 | workspace<br>捕获公共 API 和属性描述符 | BASE-01, BASE-HARNESS-01 | 核心与试点的构造/默认值/静态和实例 API 快照,其余包由 01/02 步补齐;登记契约/版本/固定测试 ID/命令/报告与负责任务 | 真实发布包反射与旧用户调用可重跑,未验证项明确 | H | done |
|
||||
| BASE-03 | workspace<br>捕获事件、异步和生命周期 | BASE-02 | ready/restart/destroy、Promise、切源/插件事件 trace;登记契约/版本/固定测试 ID/命令/报告与负责任务 | 成功、失败、重入和多实例有断言,历史缺陷有标记;使用已发布包和最小可重跑探针启动,不等待后续 ENG 服务 | H | todo |
|
||||
| BASE-04 | workspace<br>捕获 DOM、CSS 和官方 demo | BASE-02 | 核心模板/类名/变量、键盘/焦点/可访问名称基线及全 demo 路径映射;docs-browser-testing.md 中 HTML/编辑器的脚本来源与存储状态;登记契约/版本/固定测试 ID/命令/报告与负责任务 | 核心/试点用户样式和输入用法已记录,其他 demo 运行状态与未知项明确;使用已发布包和最小可重跑探针启动,不等待后续 ENG 服务 | M | todo |
|
||||
| BASE-05 | workspace<br>捕获包入口、资源和类型消费 | BASE-01 | 全包 manifest/资源路径清单;核心/试点的 UMD/AMD/ESM/CJS/legacy/i18n 和类型消费者基线;登记契约/版本/固定测试 ID/命令/报告与负责任务;区分库/npm 与站点实际分发类别,SSR import/模板/非浏览器构造错误分别捕获 | 核心/试点隔离消费可运行,其他包由各自契约任务验证,thumbnail tool 差异已登记 | H | todo |
|
||||
@@ -418,6 +418,7 @@
|
||||
- DOC-13: [记录](changes/2026-09-10-DOC-13-browser-fallback.md)
|
||||
- BASE-01: [记录](changes/2026-09-10-BASE-01-published-baseline.md) [记录](baselines/releases.json)
|
||||
- BASE-HARNESS-01: [记录](changes/2026-09-10-BASE-HARNESS-01-browser-fixture.md)
|
||||
- BASE-02: [记录](changes/2026-09-10-BASE-02-public-api.md) [记录](baselines/public-api.json) [记录](baselines/api-coverage.md)
|
||||
- ENG-01: [记录](changes/2026-09-10-ENG-01-reproducible-toolchain.md) [记录](baselines/toolchain-validation.json)
|
||||
- ENG-PM-01: [记录](changes/2026-09-10-ENG-PM-01-yarn-toolchain.md) [记录](baselines/yarn-validation.json)
|
||||
- ENG-02: [记录](changes/2026-09-10-ENG-02-readonly-ci.md) [记录](baselines/ci-validation.json)
|
||||
@@ -1,5 +1,11 @@
|
||||
# 进度与证据
|
||||
|
||||
## 当前实施:BASE-02 公共 API 基线完成
|
||||
|
||||
内置浏览器最终两次执行固定 core 5.4.0/chapter 1.1.0 的 14 项同步 API 断言,快照相同,error/warn 日志为空。已保存静态配置、默认选项、实例及继承 API、10 个子系统的描述符和来源指纹;比较器能拒绝描述符/方法/默认值等回归。见 [BASE-02](changes/2026-09-10-BASE-02-public-api.md) 与 [契约覆盖](baselines/api-coverage.md)。
|
||||
|
||||
当前 214 项任务,19 完成、195 待办。ENG-02 已提交 833cc116;本轮独立提交 BASE-02。没有生产 TS 迁移或发布结论。下一任务 BASE-03,捕获事件、异步和生命周期,并验证真实媒体场景;同步 API 快照不能替代该工作。
|
||||
|
||||
## 当前实施:ENG-02 只读检查与 CI 基础已完成
|
||||
|
||||
新增 PR/主线/复用检查与独立手动 Pages 流程;lint 与自动修复拆分,修复文档构建退出码和生成声明格式。23 项 Node/基线测试、3 个构建后导入 smoke、21 库包 63 产物与文档构建通过;426 个源/类型/测试文件只读检查前后不变,actionlint 静态和负例通过。详见 [ENG-02](changes/2026-09-10-ENG-02-readonly-ci.md) 与 [操作说明](ci-setup.md)。
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hash, refactorDir } from './releases.mjs'
|
||||
|
||||
export const checkIds = [
|
||||
'API-01.version', 'API-01.defaults-independent', 'API-01.default-language',
|
||||
'API-01.instance-registration', 'API-01.container-selector', 'API-02.video',
|
||||
'API-02.bound-query', 'API-06.sync-chapter', 'API-06.add-return',
|
||||
'API-06.result-call', 'API-04.on-return', 'API-04.off',
|
||||
'API-05.destroy-return', 'API-05.instance-cleanup',
|
||||
]
|
||||
|
||||
export function validateApiReport(report) {
|
||||
assert.equal(report.kind, 'public-api')
|
||||
assert.deepEqual(report.errors, [], 'Browser capture failed')
|
||||
assert.deepEqual(report.checks, checkIds.map(id => ({ id, passed: true })), 'Missing or failed API checks')
|
||||
assert(report.environment.userAgent && report.environment.language, 'Missing browser environment')
|
||||
assert(Number.isFinite(Date.parse(report.capture.capturedAt)), 'Missing capture timestamp')
|
||||
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
||||
const expectedSources = releases.map(release => {
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
|
||||
return { name: release.name, version: release.version, integrity: release.integrity, member, sha256: release.files[member] }
|
||||
})
|
||||
assert.deepEqual(report.capture.releases, expectedSources, 'Published sources differ')
|
||||
assert.deepEqual(report.scripts, [...expectedSources.map(release => `/releases/${release.name}/${release.member.slice('package/'.length)}`), '/fixtures/api.js'], 'Unexpected loaded scripts')
|
||||
assert.equal(report.capture.fixtureHashAlgorithm, 'sha256-lf')
|
||||
for (const name of ['api.html', 'api.js']) {
|
||||
const source = fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n')
|
||||
assert.equal(report.capture.fixtures[name], hash(source), `Fixture changed: ${name}; review the baseline explicitly`)
|
||||
}
|
||||
for (const section of ['static', 'prototype', 'emitterPrototype', 'defaults', 'constants', 'config', 'utilityDescriptors', 'instance', 'instanceResolved', 'chapterFactory', 'chapterResult', 'integrations']) {
|
||||
assert(report.snapshot?.[section] && Object.keys(report.snapshot[section]).length, `Missing API section: ${section}`)
|
||||
}
|
||||
assert.deepEqual(report.snapshot.defaults.lang, { $environment: 'navigator.language.toLowerCase()' })
|
||||
}
|
||||
|
||||
export function compareApiSnapshots(expected, actual) {
|
||||
assert.deepEqual(actual, expected, 'Public API snapshot differs; investigate instead of overwriting the baseline')
|
||||
}
|
||||
|
||||
export function verifyFrozenApi() {
|
||||
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/public-api.json'), 'utf8'))
|
||||
assert.equal(frozen.schemaVersion, 1)
|
||||
validateApiReport(frozen.report)
|
||||
assert.equal(hash(JSON.stringify(frozen.report.snapshot)), frozen.verification.snapshotSha256, 'Frozen API snapshot digest differs')
|
||||
return frozen
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const frozen = verifyFrozenApi()
|
||||
const index = process.argv.indexOf('--compare')
|
||||
if (index !== -1) {
|
||||
assert(process.argv[index + 1], '--compare requires a captured report path')
|
||||
const captured = JSON.parse(fs.readFileSync(process.argv[index + 1], 'utf8'))
|
||||
validateApiReport(captured)
|
||||
compareApiSnapshots(frozen.report.snapshot, captured.snapshot)
|
||||
console.log('Published API capture matches the frozen snapshot')
|
||||
}
|
||||
else {
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --check or --compare <report>')
|
||||
console.log(`Frozen API verified: ${checkIds.length} browser checks; browser is not rerun by this command`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { compareApiSnapshots, validateApiReport, verifyFrozenApi } from './api.mjs'
|
||||
|
||||
test('frozen API reports require passing checks and the exact published source', () => {
|
||||
const { report } = verifyFrozenApi()
|
||||
const missing = structuredClone(report)
|
||||
missing.checks.pop()
|
||||
assert.throws(() => validateApiReport(missing), /Missing or failed API checks/)
|
||||
const wrong = structuredClone(report)
|
||||
wrong.capture.releases[0].version = '999.0.0'
|
||||
assert.throws(() => validateApiReport(wrong), /Published sources differ/)
|
||||
const failed = structuredClone(report)
|
||||
failed.errors.push('constructor failed')
|
||||
assert.throws(() => validateApiReport(failed), /Browser capture failed/)
|
||||
})
|
||||
|
||||
test('API comparison detects descriptor, method and default-value regressions', () => {
|
||||
const { snapshot } = verifyFrozenApi().report
|
||||
compareApiSnapshots(snapshot, structuredClone(snapshot))
|
||||
for (const change of [
|
||||
copy => { copy.instance.play.enumerable = !copy.instance.play.enumerable },
|
||||
copy => { copy.static.NOTICE_TIME.writable = !copy.static.NOTICE_TIME.writable },
|
||||
copy => { delete copy.prototype.destroy },
|
||||
copy => { copy.defaults.volume = 0.25 },
|
||||
copy => { copy.constants.SEEK_STEP += 1 },
|
||||
copy => { delete copy.integrations.controls.resolved.show },
|
||||
copy => { copy.integrations.controls.resolved.show.set = 'undefined' },
|
||||
]) {
|
||||
const changed = structuredClone(snapshot)
|
||||
change(changed)
|
||||
assert.throws(() => compareApiSnapshots(snapshot, changed), /Public API snapshot differs/)
|
||||
}
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import { ensureArchive, readMember, refactorDir } from './releases.mjs'
|
||||
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
const root = path.resolve(refactorDir, '..')
|
||||
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8'))
|
||||
@@ -48,6 +48,16 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
const report = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
assert.equal(report.kind, 'public-api')
|
||||
report.capture = {
|
||||
capturedAt: new Date().toISOString(),
|
||||
releases: baseline.releases.map(release => ({
|
||||
name: release.name, version: release.version, integrity: release.integrity,
|
||||
member: `package/${release.manifest.main.replace(/^\.\//, '')}`,
|
||||
sha256: release.files[`package/${release.manifest.main.replace(/^\.\//, '')}`],
|
||||
})),
|
||||
fixtureHashAlgorithm: 'sha256-lf',
|
||||
fixtures: Object.fromEntries(['api.html', 'api.js'].map(name => [name, hash(fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n'))])),
|
||||
}
|
||||
const output = path.join(refactorDir, '.cache/reports')
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
fs.writeFileSync(path.join(output, 'api.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
|
||||
+6
-2
@@ -285,11 +285,15 @@
|
||||
"BASE-01",
|
||||
"BASE-HARNESS-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "核心与试点的构造/默认值/静态和实例 API 快照,其余包由 01/02 步补齐;登记契约/版本/固定测试 ID/命令/报告与负责任务",
|
||||
"acceptance": "真实发布包反射与旧用户调用可重跑,未验证项明确",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-10-BASE-02-public-api.md",
|
||||
"baselines/public-api.json",
|
||||
"baselines/api-coverage.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BASE-03",
|
||||
|
||||
Reference in new issue
Block a user