验一验:能不能读将来的档案
规范给读者立过两条义务,而在此之前没有任何东西验证过它们。加了一份「来自未来」的一致性用例,两个读取端各验一遍。以及过程中两次写出了恒真的断言。
一致性用例攒到今天有二十一份,全都在验同一个方向: 写出来的东西合不合规范。那是生产者方向。 读者方向一条都没有。
而规范第 10 节给读者立了两条义务——容忍未知字段、原样保留开放词表里的未知取值—— 写在那儿一个多月,没有任何东西检查过谁真的做到了。
要守的是哪一半
bundle/1.0 从未公开发布过,所以今天的代码读不了它是可以接受的。
真正必须守住的是反过来那一半:
今天写好的读取端,将来遇到更新的档案不能崩、不能静默丢东西。
这条听起来像是要等到真有 bundle/1.3 才能验,其实不用。
伪造一份来自未来的档案就行了——放进一致性用例里,
今天就能跑,而且以后每一版都会跑。
用例(valid/from-the-future)里放的是四样东西:
spec_version bundle/1.9 更高的小版本号
manifest future_top_level_field: {...} 顶层多一个不认识的字段
index 行 future_line_field: 42 每行也多一个
intent future.route.that.does.not.exist.yet 开放词表里的未知取值
校验器必须放行它。放不行,就说明 schema 把「只增不改」写死了——
而那正是第 10 节要避免的事。实测通过,也就顺带证明了两个 schema 的
additionalProperties 确实是敞开的,不是嘴上说说。
刻意不放进去的那一类
封闭词表(verdict / surface /
capture_fidelity)里的未知取值没有放进这份用例。
按规范,往封闭词表里加取值属于小版本变更,旧校验器拒绝它是刻意的——
所以它不属于「应通过」那一档。
读者对那一类的义务是另一条:保守处置,不得当成 ok。
「未知的判定结果」和「未知的字段」听起来像一回事,
处置方式恰好相反:前者要疑,后者要放。
一份用例,两个独立实现
用例住在规范仓库里,扩展和解析器各自去跑。这不是重复劳动:
扩展那边跑在浏览器语义上(OPFS、DecompressionStream),
解析器那边跑在 Node 上(fs、zlib),
是两份互不相干的代码在读同一份字节。
扩展这侧五条:打得开、未知字段读出来还在、未知 intent 原样保留、
逐条取出来能解压、导入扫描认得它。
顺带定下一件事:未知 intent 不报警。 它是规范预期内的情形,报警的话,将来每加一条路线, 所有旧解析器都会对着新档案刷屏——而刷屏的告警等于没有告警。
然后我两次写出了恒真的断言
这才是这一趟真正学到的东西。两份测试,各有一条断言永远不可能失败, 而且两条都绿得理直气壮。
其一,在解析器那边。写的是「产出里没有未来路线的记录」, 遍历「产出的记录」去找。可这份用例是最小用例, 它本来就产不出任何记录——那条断言遍历的是一个空数组。 遍历空集合的断言等于没写。改成正面判据:读到了、一条都没变成记录、 且一句告警都没有。
其二,在扩展那边。写的是
meta.problem ?? null === null,
而 readBundleMeta 压根不设 problem 这个字段——
把段文件整个删掉,它照样返回 null。
这条断言测的不是「没有问题」,是「这个字段不存在」。
凡是断言「某个字段没有值」,先确认那个字段在坏情况下真的会有值。 否则测的是自己的想象——而这种测试从不报错,只是一直绿着。
两处都改成了会变的判据,踩坑记在注释里。 这跟之前那个「声称有 N 条却一个 id 都没抽到」的自检是同一件事的两面: 一个从来没红过的检查,和一个不存在的检查,在证据上是等价的。