@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
1212import { dirname , join } from "node:path" ;
1313import { fileURLToPath } from "node:url" ;
1414import { describe , expect , it } from "vitest" ;
15- import { manualChunks , packageNameOf , vendorGroupOf , type VendorGroup } from "./manualChunks" ;
15+ import { EXACT , manualChunks , packageNameOf , vendorGroupOf , type VendorGroup } from "./manualChunks" ;
1616
1717const HERE = dirname ( fileURLToPath ( import . meta. url ) ) ;
1818
@@ -104,3 +104,148 @@ describe("覆盖闸:package.json 的每个运行时依赖都必须有归属",
104104 expect ( ungrouped ) . toEqual ( [ ] ) ;
105105 } ) ;
106106} ) ;
107+
108+ // ─────────────────────────────────────────────────────────────────────────────
109+ // Task 3 加固(评审 task-3-review.md Important-1 / Minor-2)。以上 40 条一字未动,
110+ // 以下全是**加测试**;实现侧只多一个 `export const EXACT` 供键集断言用,分组逻辑零改动。
111+ // ─────────────────────────────────────────────────────────────────────────────
112+
113+ /**
114+ * EXACT 表的**全量字面钉值**(Minor-2:原 49 条里 28 条无钉值 ⇒ 改错分组不报错)。
115+ * 期望值必须**字面写死**、不能从实现里读出来比对:期望若取自被审查的同一张表,
116+ * 「把 `bail` 挪进 `vendor-katex`」这类编辑会连期望值一起改,断言恒真(自指空转)。
117+ * 键集一致断言(本段最后一条)补齐另一半:新增/删除登记行也必须同步改这里。
118+ */
119+ const EXACT_PINS : readonly ( readonly [ string , VendorGroup ] ) [ ] = [
120+ [ "gsap" , "vendor-gsap" ] ,
121+ [ "@gsap/react" , "vendor-gsap" ] ,
122+ [ "react" , "vendor-react" ] ,
123+ [ "react-dom" , "vendor-react" ] ,
124+ [ "scheduler" , "vendor-react" ] ,
125+ [ "@tauri-apps/api" , "vendor-tauri" ] ,
126+ [ "@tauri-apps/plugin-dialog" , "vendor-tauri" ] ,
127+ [ "katex" , "vendor-katex" ] ,
128+ [ "rehype-katex" , "vendor-katex" ] ,
129+ [ "hast-util-from-dom" , "vendor-katex" ] ,
130+ [ "hast-util-to-text" , "vendor-katex" ] ,
131+ [ "hast-util-from-html-isomorphic" , "vendor-katex" ] ,
132+ [ "hast-util-is-element" , "vendor-katex" ] ,
133+ [ "hast-util-parse-selector" , "vendor-katex" ] ,
134+ [ "hastscript" , "vendor-katex" ] ,
135+ [ "property-information" , "vendor-katex" ] ,
136+ [ "web-namespaces" , "vendor-katex" ] ,
137+ [ "codemirror" , "vendor-editor" ] ,
138+ [ "crelt" , "vendor-editor" ] ,
139+ [ "style-mod" , "vendor-editor" ] ,
140+ [ "w3c-keyname" , "vendor-editor" ] ,
141+ [ "classcat" , "vendor-canvas" ] ,
142+ [ "zustand" , "vendor-canvas" ] ,
143+ [ "use-sync-external-store" , "vendor-canvas" ] ,
144+ [ "react-markdown" , "vendor-md" ] ,
145+ [ "unified" , "vendor-md" ] ,
146+ [ "bail" , "vendor-md" ] ,
147+ [ "trough" , "vendor-md" ] ,
148+ [ "vfile" , "vendor-md" ] ,
149+ [ "vfile-message" , "vendor-md" ] ,
150+ [ "extend" , "vendor-md" ] ,
151+ [ "is-plain-obj" , "vendor-md" ] ,
152+ [ "devlop" , "vendor-md" ] ,
153+ [ "zwitch" , "vendor-md" ] ,
154+ [ "ccount" , "vendor-md" ] ,
155+ [ "longest-streak" , "vendor-md" ] ,
156+ [ "markdown-table" , "vendor-md" ] ,
157+ [ "trim-lines" , "vendor-md" ] ,
158+ [ "escape-string-regexp" , "vendor-md" ] ,
159+ [ "decode-named-character-reference" , "vendor-md" ] ,
160+ [ "comma-separated-tokens" , "vendor-md" ] ,
161+ [ "space-separated-tokens" , "vendor-md" ] ,
162+ [ "html-url-attributes" , "vendor-md" ] ,
163+ [ "style-to-js" , "vendor-md" ] ,
164+ [ "style-to-object" , "vendor-md" ] ,
165+ [ "inline-style-parser" , "vendor-md" ] ,
166+ [ "estree-util-is-identifier-name" , "vendor-md" ] ,
167+ [ "@ungap/structured-clone" , "vendor-md" ] ,
168+ [ "@types/katex" , "vendor-katex" ] ,
169+ ] ;
170+
171+ /**
172+ * PREFIX 族的**有效**钉值(Minor-2:`^rehype-` / `^vfile` 原先没有任何由它们决定分组的
173+ * 钉值 —— `rehype-katex` / `vfile` 都被 EXACT 抢走 ⇒ 改前缀的组不会变红)。
174+ * 每个前缀一条代表;`EXACT[name]` 必须为 undefined 是**有效性判据**:代表一旦被 EXACT
175+ * 接管,这条钉值就不再约束前缀表(原缺口正是这么产生的)。
176+ */
177+ const PREFIX_PINS : readonly ( readonly [ string , VendorGroup ] ) [ ] = [
178+ [ "@codemirror/view" , "vendor-editor" ] ,
179+ [ "@lezer/common" , "vendor-editor" ] ,
180+ [ "@marijn/find-cluster-break" , "vendor-editor" ] ,
181+ [ "@xyflow/system" , "vendor-canvas" ] ,
182+ [ "d3-zoom" , "vendor-canvas" ] ,
183+ [ "micromark" , "vendor-md" ] ,
184+ [ "mdast-util-to-hast" , "vendor-md" ] ,
185+ [ "hast-util-to-jsx-runtime" , "vendor-md" ] ,
186+ [ "unist-util-visit" , "vendor-md" ] ,
187+ [ "remark-gfm" , "vendor-md" ] ,
188+ [ "rehype-raw" , "vendor-md" ] , // 未安装(惰性,同 gsap 行):冻结 `^rehype-` 族归属本身
189+ [ "vfile-location" , "vendor-md" ] , // 已随 react-markdown 装上且非 EXACT ⇒ 真由 `^vfile` 决定
190+ ] ;
191+
192+ describe ( "EXACT 表全量钉值(Minor-2:28/49 条原先无钉值)" , ( ) => {
193+ for ( const [ name , group ] of EXACT_PINS ) {
194+ it ( `EXACT ${ name } → ${ group } ` , ( ) => {
195+ expect ( EXACT [ name ] ) . toBe ( group ) ; // 表内字面值
196+ const id = `/r/node_modules/${ name } /index.js` ;
197+ expect ( vendorGroupOf ( id ) ) . toBe ( group ) ; // 解析路径:packageNameOf → EXACT
198+ expect ( manualChunks ( id ) ) . toBe ( group ) ; // 接线面:Task 4 接的正是它
199+ } ) ;
200+ }
201+
202+ it ( "EXACT 键集与钉值表逐字一致(新增/删除登记行必须同步钉值)" , ( ) => {
203+ expect ( Object . keys ( EXACT ) . sort ( ) ) . toEqual ( EXACT_PINS . map ( ( [ n ] ) => n ) . sort ( ) ) ;
204+ } ) ;
205+ } ) ;
206+
207+ describe ( "PREFIX 族有效钉值(每个前缀至少一条由它决定分组)" , ( ) => {
208+ for ( const [ name , group ] of PREFIX_PINS ) {
209+ it ( `PREFIX ${ name } → ${ group } ` , ( ) => {
210+ expect ( EXACT [ name ] ) . toBeUndefined ( ) ; // 有效性:确实由前缀表决定,未被 EXACT 接管
211+ expect ( vendorGroupOf ( `/r/node_modules/${ name } /index.js` ) ) . toBe ( group ) ;
212+ } ) ;
213+ }
214+ } ) ;
215+
216+ describe ( "确定性与无状态(Important-1:原 40 条对跨调用/跨顺序零约束)" , ( ) => {
217+ // 覆盖四类 id,且**走到末尾 `return undefined` 的未登记包恰为 2 个(偶数)**:
218+ // 「逆序重放」只有在此数为偶数时才能观察到「按调用计数」的状态 —— 若为奇数,
219+ // 正序与逆序给同一 id 的计数奇偶相同、抖动互相抵消,断言会假绿。
220+ const ids : readonly string [ ] = [
221+ "/r/node_modules/react/index.js" , // EXACT
222+ "/r/node_modules/bail/lib/index.js" , // EXACT(加固前无钉值的 28 条之一)
223+ "/r/node_modules/remark-gfm/index.js" , // PREFIX ^remark-
224+ "/r/node_modules/@xyflow/react/dist/index.js" , // EXACT(裸 token 陷阱)
225+ "/r/node_modules/katex/dist/katex.mjs" , // EXACT
226+ "/r/node_modules/not-a-real-package/index.js" , // 未登记 ①(末尾 return undefined)
227+ "/r/node_modules/zzz-also-not-real/index.js" , // 未登记 ②(同上)
228+ "/r/app/src/App.tsx" , // 非 node_modules(提前 return,不计数)
229+ "/r/vendor/node_modules_backup/react/index.js" , // 伪路径(同上)
230+ ] ;
231+
232+ it ( "同一 id 连续两次调用结果相同(计数器/缓存类跨调用状态必红)" , ( ) => {
233+ for ( const id of ids ) expect ( manualChunks ( id ) ) . toBe ( manualChunks ( id ) ) ;
234+ } ) ;
235+
236+ it ( "逆序重放后逐 id 映射与正序相同(无顺序依赖)" , ( ) => {
237+ expect ( ids . map ( manualChunks ) ) . toEqual ( [ ...ids ] . reverse ( ) . map ( manualChunks ) . reverse ( ) ) ;
238+ } ) ;
239+ } ) ;
240+
241+ describe ( "接线面全量一致(manualChunks 不许有第二套逻辑)" , ( ) => {
242+ it ( "整份混合列表上 manualChunks 与 vendorGroupOf 逐元素相同" , ( ) => {
243+ const ids = [
244+ "/r/node_modules/react/index.js" ,
245+ "/r/node_modules/bail/lib/index.js" ,
246+ "/r/node_modules/not-a-real-package/index.js" ,
247+ "/r/app/src/App.tsx" ,
248+ ] ;
249+ expect ( ids . map ( manualChunks ) ) . toEqual ( ids . map ( ( id ) => vendorGroupOf ( id ) ) ) ;
250+ } ) ;
251+ } ) ;
0 commit comments