Skip to content

Commit f3b2b6c

Browse files
committed
test(shell): 守卫B判据②改认字面量形态并收窄注释
1 parent 8d84ecf commit f3b2b6c

1 file changed

Lines changed: 51 additions & 10 deletions

File tree

‎app/src/shell/columnKeys.freeze.test.ts‎

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
*
55
* Why 单独立一条**字面量**判据:`useColumnLayout` 把 key 当作 `localStorage` 的持久化键
66
* (`layout:col-width:{key}` / `layout:col-fold:{key}`,`src/hooks/useColumnLayout.ts:43/53/87/88`)
7-
* ⇒ **改一个 key = 所有用户已记住的列宽/折叠态静默丢失**,而既有判据**抓不到**:
8-
* `columnRegistry.test.ts` ① 只判「13 行 · 键唯一 · 首尾键」—— 把 `notes-list` 改名成
9-
* `notes-list-v2` 在那条判据下**照样绿**(批 3 计划 `:1621` 想写的「键集与持久化键同形」
10-
* 最终没有落库)⇒ T0 报告 §二 · G3 把它列为**风险最高的一条缺口**。
7+
* ⇒ **改一个 key = 所有用户已记住的列宽/折叠态静默丢失**,而既有判据里**不看键名的那一条**
8+
* (`columnRegistry.test.ts` ① `:121-126`:只判「13 行 · 键唯一 · 首尾键」)**抓不到** ——
9+
* 把 `notes-list` 改名成 `notes-list-v2` 在 `①` 下**照样绿**(该文件另两处 `④` 会红:
10+
* `:157` 的 `columnSpec("notes-list")` 与 `:163+` 的「行 ↔ 档位映射」)⇒ 本条的**真实增量**
11+
* 是「冻结清单 + 顺序 + ② 前缀」,**不是**补上完全无人看守的空档(批 3 计划 `:1621` 想写的
12+
* 「键集与持久化键同形」最终没有落库)⇒ T0 报告 §二 · G3 仍把它列为**风险最高的一条缺口**。
1113
*
1214
* ★ 期望值**逐字写字符串字面量**,**不许**用 `COLUMN_KEYS` / `columnSpec(...)` / 任何被测模块的
1315
* 导出参与构造 —— 本批已实证「自引用 = 永真断言」(期望值与被测值同源 ⇒ 两边一起改照样绿)。
@@ -18,12 +20,16 @@
1820
* 重排会让 `COLUMN_SPECS` 的行序漂移);
1921
* ② 持久化键的**前缀**仍是 `layout:col-width:` / `layout:col-fold:` —— 前缀与键名是同一个
2022
* `localStorage` 键的两半,改任一半都让用户的列宽静默丢失(② 是本次为闭合该机制所加,
21-
* T0 报告 §二 · G3 只点名了 ①)。
23+
* T0 报告 §二 · G3 只点名了 ①)。判据口径 = **代码里**出现过该前缀的**连续字面量**
24+
* (**不绑书写形态**:模板字面量 / `"…" + key` 拼接 / 具名常量前缀都认;**注释里的不算数**)。
2225
*
2326
* 副作用:只读 `src/hooks/useColumnLayout.ts`(②),不修改任何文件。
2427
* 边界:本条**不**证明「这 13 个名字 = 历史持久化键」—— 那需要 `git show` 的树外一次性对拍
2528
* (见 T0 报告 §二 · G3 与批 8/T18 的收口核对表);本条是「从今往后不许再漂」的棘轮。
2629
* 也不判宽度/阈值(那住 `columnRegistry.test.ts` ②③④)。跑在 vitest 全局 `node` 环境。
30+
* ② 的口径边界(**刻意 fail-closed**):前缀若被拆成两段拼接、或搬到**别的文件**,② 会红 ——
31+
* 那是「本文件里找不到该前缀」,请连同本判据一起改,**不要**放宽成裸子串 grep
32+
* (批 1 的 6 例子串误判即由此而来)。
2733
*/
2834
import { readFileSync } from "node:fs";
2935
import { dirname, join } from "node:path";
@@ -60,12 +66,43 @@ function keyDiff(actual: readonly string[]): string[] {
6066
return out;
6167
}
6268

63-
/** ② 的捕获器:`` `layout:<名字>:${key}` `` 形态的模板字面量(读 2 处 + 写 2 处共用同一前缀)。 */
64-
const KEY_TEMPLATE = /`layout:([A-Za-z0-9_-]+):\$\{key\}`/g;
69+
/** ② 的捕获器:**代码里**出现过的持久化键前缀(`layout:<名字>:`)。
70+
* 口径 = 「前缀以**连续字面量**出现在本文件的代码里」,**不绑书写形态**:模板字面量
71+
* `` `layout:col-width:${key}` `` · 拼接 `"layout:col-width:" + key` · 具名常量前缀
72+
* `const P = "layout:col-width:"` 都认(原先只认第一种 ⇒ 行为等价的重构会假红)。
73+
* 边界见文件头「② 的口径边界」。 */
74+
const KEY_PREFIX = /layout:([A-Za-z0-9_-]+):/g;
6575

66-
/** ② 的判据本体:源码里用到的持久化键前缀集合(排序后比对;数出现次数会绑死实现形态)。 */
76+
/** 剥行注释 / 块注释(字符串与模板字面量内部原样保留)。
77+
* Why:注释里留着旧前缀不能算数 —— 前缀真从代码里搬走时,注释会把 ② 拖成假绿。 */
78+
function stripComments(src: string): string {
79+
let out = "";
80+
let i = 0;
81+
while (i < src.length) {
82+
const c = src[i];
83+
const next = src[i + 1];
84+
if (c === "/" && next === "/") {
85+
const nl = src.indexOf("\n", i);
86+
i = nl < 0 ? src.length : nl;
87+
} else if (c === "/" && next === "*") {
88+
const end = src.indexOf("*/", i + 2);
89+
i = end < 0 ? src.length : end + 2;
90+
} else if (c === '"' || c === "'" || c === "`") {
91+
let j = i + 1;
92+
while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
93+
out += src.slice(i, Math.min(j + 1, src.length));
94+
i = j + 1;
95+
} else {
96+
out += c;
97+
i++;
98+
}
99+
}
100+
return out;
101+
}
102+
103+
/** ② 的判据本体:源码里用到的持久化键前缀集合(**先剥注释**;排序后比对,数出现次数会绑死实现形态)。 */
67104
function keyPrefixes(src: string): string[] {
68-
return [...new Set([...src.matchAll(KEY_TEMPLATE)].map((m) => m[1]))].sort();
105+
return [...new Set([...stripComments(src).matchAll(KEY_PREFIX)].map((m) => m[1]))].sort();
69106
}
70107

71108
const HOOK_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "hooks", "useColumnLayout.ts");
@@ -95,8 +132,12 @@ describe("列键名冻结(改名 = 用户列宽静默丢失)", () => {
95132
"col-fold",
96133
"col-width",
97134
]);
98-
// 阴性样本:前缀改一个字符 ⇒ 同一函数立刻报出漂移
135+
// 阴性样本(三种书写形态各一):任一处前缀改一个字符 ⇒ 同一函数立刻报出漂移
99136
expect(keyPrefixes("window.localStorage.getItem(`layout:col-w:${key}`)")).toEqual(["col-w"]);
137+
expect(keyPrefixes('window.localStorage.setItem("layout:col-w:" + key, v)')).toEqual(["col-w"]);
138+
expect(keyPrefixes('const P = "layout:col-w:";')).toEqual(["col-w"]);
139+
// 仪器自检:**注释里的旧前缀不算数**(否则前缀从代码里搬走时,注释会把 ② 拖成假绿)
140+
expect(keyPrefixes('// 旧写法 `layout:col-width:${key}`\nconst P = "layout:col-w:";')).toEqual(["col-w"]);
100141
// 阳性对照:真实源码确实读到了(防路径写错 ⇒ 空集合假绿)
101142
expect(HOOK_SRC).toContain("useColumnLayout(");
102143
});

0 commit comments

Comments
 (0)