fix(权限): 409 的第二种含义是「本档不该问」——四桥都补上;状态写入点不再兜默认档
线上事故(jianf 经 pi 转达):补投路径漏传 permission_mode,插件拿 undefined 兜了 workspace 档,把 full 档会话写成 workspace-write + ask —— 不是"拦一次",是一整轮 工具能力降级,且状态留在会话里;随后该会话每次受守卫调用都撞 409。 四件事: 1. **状态写入点不接受默认值**(新增共享 `modeForStateWrite`):缺字段/脏值 → `null` = 不写状态。"默认值可以出现在**决策**里,不可以出现在**状态写入**里。" 同时保留共享契约的 fail-closed:真读到 workspace 才写 workspace。 2. **409 的两种含义分开处理**。`allowed-once` 只绕过**审批**,改不了**沙箱** —— 所以 dsh 桥在放行前先把服务端给的权威档位**写回会话**(这也就成了自愈路径: 已经降级的会话,下一次带档位的 409 会把它修回来);只认服务端明说的 full, plan 与"链上没有人类"照旧 fail closed。 3. **同一处缺陷在 zcode / opencode 也在**(`hooks/permission.mjs` 与 `index.js` 都把 409 当永久失败拒绝)。我先前在回信里写过"这两个桥不转发权限询问,不需要改" —— 那句话是错的,我当时的搜索面只有 `<plugin>/src/*.mjs`。按 pi 的要求把这条 **否定性事实变成常驻判据**后,它第一次运行就红给我看。四桥现在都有 「409 + full → 放行」,且**排在永久失败分支之前**(含顺序变异自检)。 4. **共用测试重新同源**:`test/catchup.test.mjs` 从 `153985e` 起就是分叉的 (我那版把平台专属路径写进了共用文件),而 `deploy/install.sh` 第 24 行会跑 `check-shared-libs.sh` —— 也就是说**部署一直是红的**,我没跑过那个脚本。 共用文件只放契约(值/行为),跨平台配对judge 移到平台专属文件,四份逐字节相同。 另外把"判代码 vs 判理由"从记忆变成代码:`test/lib/read.mjs` 提供 `code()/prose()/bytes()`, 判据目录里不得再裸用 `readFileSync`(新判据 `criteria-hygiene` 管,含读取器自检)。 判据证据(每条都做过"能不能红"的变异): - 写回去掉 → 红;纠正块挪到普通 409 之后 → 红;状态写入点退回兜默认 → 红; - zcode/opencode 的放行分支拿掉 → 各自红;共用测试分叉 → check-shared-libs 红。 各套件:dsh 388、pi 443、zcode 387、opencode 333(均经 npm test,含 tsc); electron `npm test` 15/15 判据绿 + vitest 266 + typecheck;`check-shared-libs.sh` 退出 0; Go `go test ./...` 全 ok。
This commit is contained in:
@ -8,17 +8,22 @@
|
||||
* 这里只断言"两边对同一件事的取值一致",不断言实现方式(WebUI 用 CSS 变量、
|
||||
* 鸿蒙用 ArkTS 常量,本来就该不同)。
|
||||
*/
|
||||
import { code, prose, stripComments } from './lib/read.mjs';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..', '..');
|
||||
const HARMONY_ETS = join(ROOT, 'client/harmony/entry/src/main/ets');
|
||||
const web = readFileSync(join(ROOT, 'client/electron/src/index.css'), 'utf8');
|
||||
const harmony = readFileSync(join(HARMONY_ETS, 'common/Theme.ets'), 'utf8');
|
||||
const web = code(join(ROOT, 'client/electron/src/index.css'));
|
||||
const harmonyPath = join(HARMONY_ETS, 'common/Theme.ets');
|
||||
/** 判「代码里有什么」用这个 */
|
||||
const harmony = code(harmonyPath);
|
||||
/** 判「注释/文档里写了什么」用这个 —— 两条判据各取所需,别混用 */
|
||||
const harmonyDoc = prose(harmonyPath);
|
||||
|
||||
/**
|
||||
* 裸色值的**类**(不只 `#RRGGBB`):四种写法一起扫 ——
|
||||
@ -26,8 +31,7 @@ const harmony = readFileSync(join(HARMONY_ETS, 'common/Theme.ets'), 'utf8');
|
||||
* 注释先剥掉:注释里引用旧写法是常有的事,而"诚实的注释"不该把判据判红。
|
||||
*/
|
||||
const RAW_COLOR = /#[0-9A-Fa-f]{6,8}\b|\brgba?\s*\(|\b0x[0-9A-Fa-f]{6,8}\b/g;
|
||||
const code = src => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
const rawColors = src => code(src).match(RAW_COLOR) || [];
|
||||
const rawColors = src => stripComments(src).match(RAW_COLOR) || [];
|
||||
|
||||
/**
|
||||
* 文档里的「有意差异」表(§7.12)。
|
||||
@ -36,7 +40,7 @@ const rawColors = src => code(src).match(RAW_COLOR) || [];
|
||||
* 不是"证明做法对"。所以跨端那几条判据的主体仍落在代码上
|
||||
* (来源必须是系统资源 / 品牌色必须是那个值),文档只做第三只手。
|
||||
*/
|
||||
const plan = readFileSync(join(ROOT, 'docs/HARMONY-ALIGN-PLAN.md'), 'utf8');
|
||||
const plan = prose(join(ROOT, 'docs/HARMONY-ALIGN-PLAN.md'));
|
||||
/**
|
||||
* 取「有意差异」那一小节。
|
||||
*
|
||||
@ -107,7 +111,7 @@ test('圆角:WebUI 有它自己的令牌,鸿蒙跟随系统 —— 差异被
|
||||
* 注意 ③ 是**弱判据**(防遗忘),不是"证明做法对"。主体是 ①②,它们落在代码上。
|
||||
*/
|
||||
assert.match(web, /--radius-card:\s*0\.875rem/, 'WebUI 侧要保留自己的圆角令牌');
|
||||
assert.match(code(harmony), /radiusCard: Resource = \$r\('sys\.float\./, '鸿蒙卡片圆角要来自系统');
|
||||
assert.match(stripComments(harmony), /radiusCard: Resource = \$r\('sys\.float\./, '鸿蒙卡片圆角要来自系统');
|
||||
assert.match(diffSection(), /圆角/, '圆角是"有意差异",要写进文档的差异表(否则会被当成漏改)');
|
||||
});
|
||||
|
||||
@ -115,7 +119,7 @@ test('材质:WebUI 声明透明度令牌,鸿蒙用系统材质 —— 差异
|
||||
// 同上:旧口径钉"两边都 0.72"。鸿蒙改用 `BlurStyle` 后,"0.72 的白色"这件事
|
||||
// 由系统按主题决定(深浅各一套),所以取值**允许**不同,只要求"材质来自系统"且差异被记录。
|
||||
assert.match(web, /--nav-bg:\s*255 255 255 \/ 0\.72/, 'WebUI 侧要保留自己的导航底令牌');
|
||||
assert.match(code(harmony), /navMaterial: BlurStyle = BlurStyle\./, '鸿蒙导航材质要来自系统');
|
||||
assert.match(stripComments(harmony), /navMaterial: BlurStyle = BlurStyle\./, '鸿蒙导航材质要来自系统');
|
||||
assert.match(diffSection(), /材质/, '材质是"有意差异",要写进文档的差异表');
|
||||
});
|
||||
|
||||
@ -126,7 +130,7 @@ test('★ 判据自检:把鸿蒙的品牌蓝改成别的必须判红', () => {
|
||||
});
|
||||
|
||||
test('鸿蒙的令牌文件说明了与 WebUI 的对应关系(不是凭空一套)', () => {
|
||||
assert.match(harmony, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/);
|
||||
assert.match(harmonyDoc, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/, '这条判的是**注释里写的对应关系**,所以要读原文(prose),不是剥离版');
|
||||
});
|
||||
|
||||
test('★ 鸿蒙页面里不得出现任何裸色值(枚举挡不住漂移,"类"才能挡)', () => {
|
||||
@ -148,7 +152,7 @@ test('★ 鸿蒙页面里不得出现任何裸色值(枚举挡不住漂移,"
|
||||
const files = readdirSync(dir).filter(f => f.endsWith('.ets')).sort();
|
||||
assert.ok(files.length >= 8, `pages/ 下只扫到 ${files.length} 个 .ets,判据大概扫错了目录`);
|
||||
for (const f of files) {
|
||||
const hits = rawColors(readFileSync(join(dir, f), 'utf8'));
|
||||
const hits = rawColors(prose(join(dir, f)));
|
||||
assert.equal(hits.length, 0, `${f} 里有裸色值 ${hits.join('、')}(应改用 Theme 令牌或系统资源)`);
|
||||
}
|
||||
// 反向对照:四种形态都要抓得到(否则写错了正则也是一片绿)
|
||||
@ -192,7 +196,7 @@ test('A|系统拥有的维度,鸿蒙侧的唯一来源是系统资源(不
|
||||
// 反向断言:这些格子不得退回 string/number 类型(退回 = 又开始自己定值了)
|
||||
for (const name of Object.keys(sysOwned)) {
|
||||
assert.ok(
|
||||
!new RegExp(`${name}: (string|number) =`).test(code(harmony)),
|
||||
!new RegExp(`${name}: (string|number) =`).test(stripComments(harmony)),
|
||||
`${name} 不再是系统资源了 —— 这是"用系统方案"被回退的信号`
|
||||
);
|
||||
}
|
||||
@ -220,7 +224,7 @@ const SELF_OWNED_COLORS = [
|
||||
];
|
||||
|
||||
test('A2|Theme.ets 里"自己写的色"必须**登记过**:新写死一个色不该默默通过', () => {
|
||||
const themeSrc = code(harmony);
|
||||
const themeSrc = harmony; // 声明在代码里:读剥离版就够了
|
||||
const declared = [...themeSrc.matchAll(/static readonly (\w+): string = '(#[0-9A-Fa-f]{6,8})'/g)].map(m => m[1]);
|
||||
assert.ok(declared.length >= 10, `要从 Theme.ets 里读到那些手写色,实际读到 ${declared.length} 个`);
|
||||
|
||||
@ -240,11 +244,11 @@ test('A2|Theme.ets 里"自己写的色"必须**登记过**:新写死一个
|
||||
* 不是剥过注释的源码(剥注释会把登记表本身剥掉,第一版就踩了这个:
|
||||
* `indexOf` 返回 -1,切片拿到一段不相干的东西)。
|
||||
*/
|
||||
const regStart = harmony.indexOf('手写色**登记表**');
|
||||
const regStart = harmonyDoc.indexOf('手写色**登记表**');
|
||||
assert.ok(regStart > 0, 'Theme.ets 里要有「手写色登记表」那一段');
|
||||
const regEnd = harmony.indexOf('品牌色:跨客户端身份', regStart);
|
||||
const regEnd = harmonyDoc.indexOf('品牌色:跨客户端身份', regStart);
|
||||
assert.ok(regEnd > regStart, '登记表那一段要有明确的结束边界(下一个分节标题)');
|
||||
const registry = harmony.slice(regStart, regEnd);
|
||||
const registry = harmonyDoc.slice(regStart, regEnd);
|
||||
assert.ok(registry.length > 200, `登记表太短,可能切错了段落(${registry.length} 字符)`);
|
||||
for (const name of SELF_OWNED_COLORS) {
|
||||
assert.ok(registry.includes(name), `登记表里要提到 ${name}(否则理由只存在于写它那个人的记忆里)`);
|
||||
@ -265,7 +269,7 @@ test('A2|Theme.ets 里"自己写的色"必须**登记过**:新写死一个
|
||||
});
|
||||
|
||||
test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与 WebUI 绑死的圆角数字', () => {
|
||||
const codeOnly = code(harmony);
|
||||
const codeOnly = stripComments(harmony);
|
||||
/*
|
||||
* `navBgLight` / `navBgDark` 这两个名字本身就是罪证:浅色一个、深色一个手写玻璃,
|
||||
* 等于"我们替系统猜了深色该怎么做"。pi 在 WebUI 侧撤掉 `.dark` 导航令牌、
|
||||
@ -353,7 +357,7 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
* (并列本身没问题),而真正的嵌套它没在判。P5 正是"悬浮玻璃导航",很可能撞上第二处;
|
||||
* 到那时若把 1 改成 2,这条就退化成"最多两处"(等于不判)。所以现在就把形状改对。
|
||||
*/
|
||||
const codeOnly = code(harmony);
|
||||
const codeOnly = stripComments(harmony);
|
||||
assert.match(codeOnly, /static readonly navMaterial: BlurStyle = BlurStyle\.[A-Z_]+/, '导航材质要声明成系统材质档次');
|
||||
const navMaterial = codeOnly.match(/navMaterial: BlurStyle = BlurStyle\.([A-Z_]+)/)[1];
|
||||
assert.notEqual(navMaterial, 'NONE', 'NONE 等于没有材质,"玻璃"就名存实亡');
|
||||
@ -362,7 +366,7 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
/** 每个调用点:哪一处(文件#组件)、调用下标、它作用的**组件块**(配对出来的) */
|
||||
const found = [];
|
||||
for (const f of collectEts(etsRoot)) {
|
||||
const src = code(readFileSync(f, 'utf8'));
|
||||
const src = code(f);
|
||||
const spans = braceSpans(src);
|
||||
for (const m of src.matchAll(/backgroundBlurStyle\(/g)) {
|
||||
const at = m.index;
|
||||
@ -414,7 +418,7 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
}
|
||||
|
||||
// 导航条那一处必须真的还在(形状判定之外,位置本身也要在)
|
||||
const main = code(readFileSync(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets'), 'utf8'));
|
||||
const main = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets'));
|
||||
assert.match(main, /\.backgroundBlurStyle\(Theme\.navMaterial\)/, '导航条要用系统材质(不是手写 alpha)');
|
||||
|
||||
/*
|
||||
@ -439,22 +443,19 @@ test('★ 品牌色防线:主操作色不得退化成系统强调色', () => {
|
||||
*/
|
||||
assert.match(harmony, /accent: string = '#2563EB'/, '品牌蓝必须仍是 WebUI 的那个值');
|
||||
assert.ok(
|
||||
!/accent: Resource/.test(code(harmony)),
|
||||
!/accent: Resource/.test(stripComments(harmony)),
|
||||
'accent 变成系统资源了 —— 品牌色跟着系统变就不再是同一个产品的标识'
|
||||
);
|
||||
// 主操作/选中态仍引用它(否则"没退化"只是因为它没被用 —— 值留着也没意义)
|
||||
const login = code(readFileSync(join(ROOT, 'client/harmony/entry/src/main/ets/pages/LoginPage.ets'), 'utf8'));
|
||||
const login = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/LoginPage.ets'));
|
||||
assert.match(login, /backgroundColor\(Theme\.accent\)/, '登录按钮仍是品牌色');
|
||||
const main = code(readFileSync(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets'), 'utf8'));
|
||||
const main = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets'));
|
||||
assert.match(main, /backgroundColor\(Theme\.accent\)/, '主操作按钮/选中态仍是品牌色');
|
||||
});
|
||||
|
||||
test('权限档位徽标两边同一套色(plan=蓝 / workspace=绿 / full=琥珀)', () => {
|
||||
// WebUI 的映射写在 PermissionChip.tsx 的类名里;鸿蒙的映射是 Theme.permBg/permFg。
|
||||
const chip = readFileSync(
|
||||
join(ROOT, 'client/electron/src/components/PermissionChip.tsx'),
|
||||
'utf8'
|
||||
);
|
||||
const chip = code(join(ROOT, 'client/electron/src/components/PermissionChip.tsx'));
|
||||
assert.match(chip, /plan'[\s\S]{0,80}bg-blue-50 text-blue-700/, 'WebUI plan 档应是蓝');
|
||||
assert.match(chip, /full'[\s\S]{0,80}bg-amber-50 text-amber-700/, 'WebUI full 档应是琥珀');
|
||||
assert.match(chip, /bg-green-50 text-green-700/, 'WebUI workspace 档应是绿');
|
||||
@ -513,10 +514,10 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在
|
||||
* 深浅两套值由系统给,而且不会再漏配一边 —— 比我们自己维护两个常量更不容易错。
|
||||
* 所以口径改成"遮罩来自系统"(这条是硬的),WebUI 侧仍然两段式(它没有系统可跟随)。
|
||||
*/
|
||||
assert.match(code(harmony), /overlay: Resource = \$r\('sys\.color\.ohos_id_color_mask_regular'\)/, '鸿蒙遮罩要用系统遮罩色');
|
||||
assert.match(stripComments(harmony), /overlay: Resource = \$r\('sys\.color\.ohos_id_color_mask_regular'\)/, '鸿蒙遮罩要用系统遮罩色');
|
||||
// 不许再自己维护"色 + 透明度"两个常量(那正是系统已经替我们做掉的事)
|
||||
assert.ok(!/overlayColor: string/.test(code(harmony)), '遮罩色不该再由我们自己定');
|
||||
assert.ok(!/overlayAlpha: number/.test(code(harmony)), '遮罩透明度不该再由我们自己定');
|
||||
assert.ok(!/overlayColor: string/.test(stripComments(harmony)), '遮罩色不该再由我们自己定');
|
||||
assert.ok(!/overlayAlpha: number/.test(stripComments(harmony)), '遮罩透明度不该再由我们自己定');
|
||||
|
||||
/*
|
||||
* pi 读出来的第四条:判据只断言 `overlay` **存在**,没断言它**被用** ——
|
||||
@ -528,13 +529,13 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在
|
||||
const themePath = join(HARMONY_ETS, 'common/Theme.ets');
|
||||
const overlayUsers = collectEts(etsRoot)
|
||||
.filter(f => f !== themePath)
|
||||
.filter(f => /Theme\.overlay\b/.test(code(readFileSync(f, 'utf8'))))
|
||||
.filter(f => /Theme\.overlay\b/.test(code(f)))
|
||||
.map(f => f.slice(etsRoot.length + 1));
|
||||
assert.ok(overlayUsers.length > 0,
|
||||
'Theme.overlay 声明了却没有任何使用点 —— 那就是个死令牌(要么删掉,要么写出它的使用处)');
|
||||
// 用它的必须是**自绘遮罩**的地方(系统自带遮罩的弹窗不需要它)
|
||||
assert.ok(overlayUsers.every(f => f.endsWith('.ets')), `遮罩使用点应该是页面:${overlayUsers.join('、')}`);
|
||||
assert.ok(!/#[0-9A-Fa-f]{8}/.test(code(harmony)), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
|
||||
assert.ok(!/#[0-9A-Fa-f]{8}/.test(stripComments(harmony)), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
|
||||
// WebUI 侧同构:颜色两套(浅/深,同一个变量名换向)+ 透明度独立
|
||||
assert.match(web, /--bg-scrim: 255 255 255/, 'WebUI 浅色遮罩色');
|
||||
assert.match(web, /--bg-scrim: 0 0 0/, 'WebUI 深色遮罩色');
|
||||
@ -624,7 +625,7 @@ test('★ 手写色清册**跨文件**:全 ets 树里每个 `X: string = \'#RR
|
||||
const declared = [];
|
||||
for (const f of tree) {
|
||||
const rel = f.slice(HARMONY_ETS.length + 1);
|
||||
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
const src = prose(f).replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const m of src.matchAll(/(\w+)\s*:\s*string\s*=\s*'(#[0-9A-Fa-f]{6})'/g)) {
|
||||
declared.push({ file: rel, name: m[1], value: m[2] });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user