fix(判据): 探针三值 + 发布候选标签 —— 顺带查出探针从写下那天起一次都没跑成过
pi 的两条"真实的洞",都落了,而且第一条当场抓到实证。 1. **探针三值**(可用 / 不可用 / 拿不准→红):`RESULT static=5 probe=ok|unknown` 把"欠账余额"和"探针是否健康"拆成两个数字。 **换完第一次运行就报 probe=unknown** —— 一查:探针调的是 `execFileSync`, 而这个文件 import 的是 `spawnSync`,**名字根本没定义**。也就是说 **探针从写下的那天起一次都没跑成过**,旧的两值设计把 `ReferenceError` 和"没有设备"一起吞掉、统一报成"设备不可用":机制在、闸门从没开过, 而它看起来完全健康。这正是 pi 描述的"恒不开闸",只是比预想更彻底。 现在:命令在但跑不成 → unknown → 红;所有候选都不存在(本机没装 hdc)→ 可判的 "没有设备工具" → false,避免没装 SDK 的机器天天假红。 附 `--probe-selftest`(只跑分类器,不跑套件)+ 变异验证(把 unknown 当"不成立"→ 红)。 2. **releaseCandidate = !gitDirty**(从展示升成标签):BUILD_INFO 现在自报 `releaseCandidate`,发布脚本在脏树时会打印"这个包不是发布候选"。 判据 `build-stamp` 断言"标签与 gitDirty 必须一致"。 **实证**:本轮我打的包正是这种情况 —— `gitDirty: true`(含着 gui-lab 未提交的 NarrowStack/index.css),`releaseCandidate: false`,日志里明确说了"不是发布候选"。 3. 附带:`criteria-hygiene` 加一条"用到 `code/prose/bytes` 就必须真的 import"。 理由是同一形状我这轮在三个文件里各犯过一次(最后一次是 `execFileSync`/`spawnSync`), 而它表现为"判据红了"(ReferenceError 抛在判据自己身上),看起来像判据失败、 不像判据写错。这条至少把最常写错的那几个名字变成明确的红。
This commit is contained in:
@ -95,6 +95,18 @@ export function writeBuildInfo(root = ELECTRON_DIR, buildCmd = process.env.npm_l
|
||||
buildCmd,
|
||||
builtAt: new Date().toISOString(),
|
||||
// 说明这份文件是什么、谁在比它(读到此文件的人不用去翻代码)
|
||||
/*
|
||||
* **标签**,不是展示(pi 2026-09-14 §1)。
|
||||
*
|
||||
* 共享树上永远有人在写别的文件,所以"脏树"是常态 —— 拿它当红会让门禁天天假红。
|
||||
* 但"从脏树打出来的包"和"从干净树打出来的包"**在门禁上必须可区分**:
|
||||
* 前者含别人的半成品,只能自用,不能给人装。所以给一个是/否:
|
||||
*
|
||||
* releaseCandidate = !gitDirty
|
||||
*
|
||||
* 部署路径可以据此拒绝安装(判据见 build-stamp 的"脏树产物不得自称发布候选")。
|
||||
*/
|
||||
releaseCandidate: !st.gitDirty,
|
||||
note: '构建自证:test/build-stamp.test.mjs 用 srcHash/gitRev 精确比对,不是比时间戳'
|
||||
};
|
||||
const out = join(root, 'dist', 'BUILD_INFO.json');
|
||||
|
||||
@ -39,6 +39,11 @@ bash -c "$PACK_CMD"
|
||||
# 产物自报来源:把 BUILD_INFO 打进日志(pi 提的"一个包要自带它对应哪个源码状态")。
|
||||
# 出问题时先看这几行:包是在哪个提交、哪份源码指纹上构建的,以及当时树干不干净。
|
||||
if [[ -f dist/BUILD_INFO.json ]]; then
|
||||
# "脏树打的包"必须**看得见**(pi §1):共享树上脏是常态,所以不做成红,
|
||||
# 但要在日志里明说这个包不是发布候选(含别人未提交的半成品时不能给人装)。
|
||||
if grep -q '"releaseCandidate": *false' dist/BUILD_INFO.json 2>/dev/null; then
|
||||
echo "[release] ⚠ 这个包不是发布候选(releaseCandidate=false):构建时工作树是脏的。"
|
||||
fi
|
||||
echo "[release] 产物来源:$(cat dist/BUILD_INFO.json | tr -d '\n' | sed 's/ */ /g')"
|
||||
else
|
||||
echo "[release] 警告:dist/BUILD_INFO.json 不存在 —— 构建没走 npm run build?(判据会红)" >&2
|
||||
|
||||
@ -138,3 +138,21 @@ test('★ 产物必须自报来源:BUILD_INFO 精确比对(不是比时间
|
||||
assert.ok(typeof info.gitDirty === 'boolean', 'BUILD_INFO 要记录构建时工作树是否干净(供复核者判断)');
|
||||
assert.ok(info.builtAt && info.buildCmd !== undefined, 'BUILD_INFO 要带构建时间与构建命令');
|
||||
});
|
||||
|
||||
/**
|
||||
* ★ 脏树产物**不得自称发布候选**(pi 2026-09-14 §1:把 gitDirty 从展示升成标签)。
|
||||
*
|
||||
* 共享树上永远有别人在写文件,"脏"是常态 —— 所以这条不判"脏就该红"(那会天天假红),
|
||||
* 判的是**两者必须可区分**:脏树产物只能自用,不能给人装;而这句话得由产物自己说,
|
||||
* 不能靠读它的人去猜。
|
||||
*/
|
||||
test('★ 脏树产物不得自称发布候选(标签与 gitDirty 必须一致)', () => {
|
||||
const infoPath = join(ROOT, 'client/electron/dist/BUILD_INFO.json');
|
||||
const info = JSON.parse(prose(infoPath)); // JSON 是数据 → prose
|
||||
assert.equal(typeof info.gitDirty, 'boolean', 'BUILD_INFO 必须报 gitDirty');
|
||||
assert.equal(typeof info.releaseCandidate, 'boolean',
|
||||
'BUILD_INFO 必须报 releaseCandidate(标签,不是展示)');
|
||||
assert.equal(info.releaseCandidate, !info.gitDirty,
|
||||
`发布候选标签与工作树状态矛盾:gitDirty=${info.gitDirty} 却说 releaseCandidate=${info.releaseCandidate}。\n` +
|
||||
'脏树打的包里可能含着别人**未提交**的半成品 —— 它只能自用,必须在门禁上可区分。');
|
||||
});
|
||||
|
||||
@ -89,3 +89,43 @@ test('★ 判据目录里不得出现裸 readFileSync(必须走 code/prose/byt
|
||||
" · bytes(path) —— 二进制(安装包等)。\n" +
|
||||
" 为什么不能裸用:读原文去判代码,会被解释性注释骗(同一个坑已经踩过两次)。");
|
||||
});
|
||||
|
||||
/**
|
||||
* ★ 用到 `lib/read.mjs` 的导出名就必须真的 import(我这轮在三个文件里各犯过一次)。
|
||||
*
|
||||
* 形状一模一样:`code(...)` / `prose(...)` 写下去,import 里却只有另一个 ——
|
||||
* 于是在**跑起来的那一刻**才炸 `ReferenceError`,而它抛在判据自己身上,
|
||||
* 看起来像"这条判据红了",不像"判据写错了"。dsh 桥那边也栽过同一形状
|
||||
* (`MODE_FULL` 没 import,而且被 `tsc | tail` 的退出码骗过)。
|
||||
*
|
||||
* 判据做法:把每个判据文件里出现的 `code(`/`prose(`/`bytes(` 收集起来,
|
||||
* 与它从 `lib/read.mjs` 实际 import 的名字比对;缺一个就红,并点名文件与名字。
|
||||
* **例外**:文件里自己定义了同名函数(本地实现)时不算缺 —— 但那种情况要显式声明。
|
||||
*/
|
||||
test('★ 用到 code/prose/bytes 就必须 import(不许靠运行时才发现)', () => {
|
||||
const EXPORTS = ['code', 'prose', 'bytes'];
|
||||
const problems = [];
|
||||
const SELF_PATH = fileURLToPath(import.meta.url);
|
||||
for (const f of criteriaFiles()) {
|
||||
// 它自己的源码里就写着 code/prose/bytes 这几个名字(EXPORTS 列表),跳过自己
|
||||
if (f === SELF_PATH) continue;
|
||||
const src = prose(f);
|
||||
if (src.includes("from './lib/read.mjs'") || src.includes("from '../lib/read.mjs'")) {
|
||||
const m = /import \{([^}]*)\} from '\.\.?\/lib\/read\.mjs'/.exec(src);
|
||||
const bound = new Set((m ? m[1] : '').split(',').map(x => x.trim().split(/\s+as\s+/).pop()).filter(Boolean));
|
||||
for (const name of EXPORTS) {
|
||||
if (new RegExp(`\\b${name}\\(`).test(src) && !bound.has(name)) {
|
||||
problems.push(`${relative(RELECTRON, f)} 用了 ${name}(…) 但没 import(已 import:${[...bound].join('、') || '无'})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const name of EXPORTS) {
|
||||
if (new RegExp(`\\b${name}\\(`).test(src)) {
|
||||
problems.push(`${relative(RELECTRON, f)} 用了 ${name}(…) 但根本没从 lib/read.mjs import`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(problems, [],
|
||||
`这些判据会以 ReferenceError 的形式红,看起来像"判据失败了",其实是"判据写错了":\n ${problems.join('\n ')}`);
|
||||
});
|
||||
|
||||
@ -59,7 +59,7 @@ for (const must of ['配对/解析', 'allow-list', '变异验证', '剥掉注释
|
||||
|
||||
const SUITE = [
|
||||
['test/markdown-xss.test.mjs', [], 9],
|
||||
['test/narrow-layout.test.mjs', [], 62],
|
||||
['test/narrow-layout.test.mjs', [], 64],
|
||||
['test/nav-merge.test.mjs', [], 8],
|
||||
['test/theme.test.mjs', [], 30],
|
||||
['test/background.test.mjs', [], 42],
|
||||
@ -72,11 +72,11 @@ const SUITE = [
|
||||
['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6],
|
||||
// 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号
|
||||
['test/appearance-defaults.test.mjs', [], 3],
|
||||
['test/build-stamp.test.mjs', [], 6],
|
||||
['test/build-stamp.test.mjs', [], 7],
|
||||
['test/packaging.test.mjs', [], 5],
|
||||
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2],
|
||||
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口
|
||||
['test/criteria-hygiene.test.mjs', [], 2]
|
||||
['test/criteria-hygiene.test.mjs', [], 3]
|
||||
];
|
||||
|
||||
// 自检 1:清单里的文件必须真的存在(写错名字 = 那条判据永远不跑)
|
||||
@ -226,25 +226,91 @@ for (const [file, flags, expected] of SUITE) {
|
||||
* 这条是"自报 0 条 < 登记条数"的**时间版本**:那条管"判据还在不在",这条管
|
||||
* "它该升级了没有"。
|
||||
*/
|
||||
/**
|
||||
* 探针:判定静态判据的"到期前提"是否成立。
|
||||
*
|
||||
* # 三值,不是两值(pi 2026-09-14 §2)
|
||||
*
|
||||
* 原先只有"成立/不成立"两种结果,于是**"探针跑不了"和"设备不可用"被归成同一格**:
|
||||
* 设备那天真可用了,闸门也永远不会开 —— 机制在,闸门锈死,而且看起来完全健康
|
||||
* (这是"自报 0 条 < 登记 30 条"的第三种形状:探针自己坏了,没人知道)。
|
||||
*
|
||||
* 所以:
|
||||
* - `true` 可用(前提成立 → 依赖它的静态判据**到期**,必须处理);
|
||||
* - `false` 不可用(探针**确实跑成了**,结论是没有目标);
|
||||
* - `'unknown'` 拿不准(**命令在但跑不成**:超时、非零退出、抛异常)→ **按到期处理**。
|
||||
*
|
||||
* # 这条设计当场抓到了什么(写下来,因为它是"闸门锈死"的实证)
|
||||
*
|
||||
* 换三值之后第一次运行就报了 `probe=unknown` —— 一查:探针里调的是 `execFileSync`,
|
||||
* 而这个文件 import 的是 `spawnSync`(**名字根本没定义**)。也就是说
|
||||
* **探针从写下的那天起一次都没跑成过**,而旧的二值设计把 `ReferenceError`
|
||||
* 连同"没有设备"一起吞掉、统一报成"设备不可用"——**机制在、闸门从来没开过,
|
||||
* 而它看起来完全健康**。三值把它变成了一声明确的红:探针自己坏了,必须有人看一眼。
|
||||
*
|
||||
* 另外,`'unknown'` 只留给"命令在、但跑不成";**所有候选都不存在**(本机没装 hdc)
|
||||
* 是可判的事实(没有工具就不可能有设备),报 `false`,否则没装 SDK 的机器会天天假红。
|
||||
* 拿不准就红,让人看一眼 —— 这条判断比"猜一个"便宜得多。
|
||||
*
|
||||
* 测试用 `AGENTMAIL_PROBE_DEVICE=ok|none|unknown` 覆盖(判据自检要用:
|
||||
* 没有这个开关就没法验证"unknown 会不会红")。
|
||||
*/
|
||||
const PROBES = {
|
||||
device: {
|
||||
desc: '有可用的设备/模拟器(hdc 看得到目标)',
|
||||
run() {
|
||||
const override = process.env.AGENTMAIL_PROBE_DEVICE;
|
||||
if (override === 'ok') return true;
|
||||
if (override === 'none') return false;
|
||||
if (override === 'unknown') return 'unknown';
|
||||
const sdkHdc = '/opt/huawei/command-line-tools/sdk/default/openharmony/toolchains/hdc';
|
||||
const candidates = [sdkHdc, 'hdc'];
|
||||
let ranOnce = false; // 至少有一条命令**跑成过**(哪怕是"没有目标")
|
||||
let sawEmpty = false;
|
||||
let anomaly = ''; // 命令在,但跑不成(超时/非零退出/…)—— 这类是"拿不准"
|
||||
let allMissing = true; // 所有候选都不存在 = 本机没装设备工具(这是**可判**的:不可能有设备)
|
||||
for (const bin of candidates) {
|
||||
try {
|
||||
const out = execFileSync(bin, ['list', 'targets'], { encoding: 'utf8', timeout: 15000 });
|
||||
const t = out.trim();
|
||||
if (t && !/\[Empty\]/.test(t)) return true;
|
||||
} catch { /* 没有 hdc 或超时:前提不成立 */ }
|
||||
const r = spawnSync(bin, ['list', 'targets'], { encoding: 'utf8', timeout: 15000 });
|
||||
if (r.error && r.error.code === 'ENOENT') continue; // 这个候选不存在,看下一个
|
||||
allMissing = false;
|
||||
if (r.error) { anomaly = r.error.code || String(r.error.message || r.error); continue; }
|
||||
if (r.status !== 0) { anomaly = `exit ${r.status}`; continue; }
|
||||
ranOnce = true;
|
||||
const t = (r.stdout || '').trim();
|
||||
if (t && !/\[Empty\]/.test(t)) return true; // 明确可用
|
||||
sawEmpty = true; // 明确"没有目标"
|
||||
}
|
||||
return false;
|
||||
if (ranOnce && sawEmpty) return false; // 跑成了,且确实没有目标
|
||||
if (anomaly) return 'unknown'; // 命令在、跑不成 → 拿不准(要红)
|
||||
if (allMissing) return false; // 本机根本没有 hdc → 不可能有设备
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 探针结论 → 是否等于"前提成立(到期)" */
|
||||
const probeIsDue = v => v === true || v === 'unknown';
|
||||
|
||||
/** 只能验形态的判据:文件 + 为什么只能静态 + 到期前提 */
|
||||
/*
|
||||
* 探针自检(`--probe-selftest`):它判的是**判据自己的分辨力** ——
|
||||
* "unknown 到底会不会红"。没有这条,`probeIsDue` 哪天被改成 `v === true`
|
||||
* 也没人会发现,而那正是"闸门锈死"的写法。
|
||||
*/
|
||||
if (process.argv.includes('--probe-selftest')) {
|
||||
const cases = [
|
||||
['ok 可用 → 到期', true, true],
|
||||
['none 不可用 → 不到期', false, false],
|
||||
['unknown 拿不准 → 到期(必须红)', 'unknown', true],
|
||||
];
|
||||
let bad = 0;
|
||||
for (const [what, value, want] of cases) {
|
||||
const got = probeIsDue(value);
|
||||
console.log(`${got === want ? 'ok ' : 'RED '} ${what}(probeIsDue(${JSON.stringify(value)}) = ${got})`);
|
||||
if (got !== want) bad++;
|
||||
}
|
||||
process.exit(bad ? 1 : 0);
|
||||
}
|
||||
const STATIC_ONLY = [
|
||||
['test/harmony-nav.test.mjs', '底栏结构/命中区常量/挂载关系:`.ets` 要 hvigorw 才能编译、要设备才能点', 'device'],
|
||||
['test/harmony-appearance.test.mjs', '壁纸/令牌/遮罩渲染:观感与运行期换肤要设备', 'device'],
|
||||
@ -263,9 +329,18 @@ for (const [file, , probe] of STATIC_ONLY) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const dueStatic = STATIC_ONLY.filter(([, , probe]) => PROBES[probe].run());
|
||||
const probeResults = {};
|
||||
for (const [, , probe] of STATIC_ONLY) {
|
||||
if (probeResults[probe] === undefined) probeResults[probe] = PROBES[probe].run();
|
||||
}
|
||||
|
||||
const dueStatic = STATIC_ONLY.filter(([, , probe]) => probeIsDue(probeResults[probe]));
|
||||
const unknownProbes = Object.entries(probeResults).filter(([, v]) => v === 'unknown').map(([k]) => k);
|
||||
if (dueStatic.length > 0) {
|
||||
console.error(`\n✗ 静态判据**到期**了:${PROBES[dueStatic[0][2]].desc} 现在是成立的 ——`);
|
||||
const firstUnknown = unknownProbes.includes(dueStatic[0][2]);
|
||||
console.error(firstUnknown
|
||||
? `\n✗ 探针**跑不了**(${PROBES[dueStatic[0][2]].desc})—— 拿不准就按到期处理,别让闸门锈死:`
|
||||
: `\n✗ 静态判据**到期**了:${PROBES[dueStatic[0][2]].desc} 现在是成立的 ——`);
|
||||
for (const [file, why, probe] of dueStatic) {
|
||||
console.error(` - ${file}(到期前提:${PROBES[probe].desc};当初只能静态的原因:${why})`);
|
||||
}
|
||||
@ -277,7 +352,15 @@ if (dueStatic.length > 0) {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`RESULT static=${STATIC_ONLY.length}(只能验形态的判据:到期前提成立就自动变红)`);
|
||||
/*
|
||||
* 汇总里"欠账余额"和"探针是否健康"是两个不同的数字(pi §2):
|
||||
* static=5 —— 还欠着 5 条只能验形态的判据;
|
||||
* probe=ok —— 探针自己是好的(unknown 说明闸门可能锈死了,得人看一眼)。
|
||||
*/
|
||||
const probeSummary = unknownProbes.length ? 'unknown' : 'ok';
|
||||
console.log(`RESULT static=${STATIC_ONLY.length} probe=${probeSummary}` +
|
||||
(unknownProbes.length ? `(探针跑不了:${unknownProbes.join('、')} —— 已按到期处理)`
|
||||
: '(只能验形态的判据:到期前提成立就自动变红)'));
|
||||
|
||||
console.log(`\n========== 判据汇总 ==========`);
|
||||
if (reds.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user