跨端: 修打包坑(构建失败被管道吞掉)+ dist 新鲜度判据;判据从"窗口式"改成同表达式
接手 pi 的 WebUI 开项途中踩到三个坑,都已修好并配判据(承接 e94313f)。 ## 坑 1:`npm run build 2>&1 | tail -4 && electron-builder …` 吞掉构建失败 pipeline 的退出码是 `tail` 的 → 构建失败、`&&` 照样往下走、electron-builder **拿旧的 dist 打了新包**。而所有判据都是绿的: - vitest 绿(见坑 2);packaging 绿(它比 dist vs 安装包,两边都是旧的,自然一致)。 新增判据:**dist 必须比 src 新**(build-stamp)。变异:`touch` 一个 src 文件 → 红; 重新 `npm run build` → 绿。错误信息写明"注意别把它的退出码丢在管道里"。 已重构建 + 重打包(deb 与 app.asar 同批,15:19)。 ## 坑 2:测试通过 ≠ 能打包 真正的失败:`AGGREGATE_ID` / `isUsableAccount` 被我当成 `accountStore` 的导出 (它们住在 `lib/accounts.ts`)。vitest 263 条全绿,生产构建直接报 `"AGGREGATE_ID" is not exported by "src/stores/accountStore.ts"` —— 测试运行时对缺的具名导出是宽容的(拿到 undefined)。**生产构建是一道独立的门。** ## 坑 3:判据写成"窗口式",被自己的变异测试抓住两次 `appearance-defaults` 的"取键函数必须把账号拼进去": 1. 第一版从 `export function` 切到 `}` → **参数表里的 accountId 满足了正则**, 把实现退回全局键仍然全绿(假判据!); 2. 第二版只取函数体 → `return accountId ? PREFIX : PREFIX;`(提了一下没用)又骗过去; 3. 第三版要求**同一个表达式里既有常量又有账号的插值/拼接**: 两种退化都红,合法的 `PREFIX + accountId` 写法仍然绿。 三种变异都验过(红/红/绿),还原后基线绿。 ## 文档 §7.12 两行改为「已修」并写明依据(默认值=服务端契约;缓存键差异已消除,旧全局键 只作一次性迁移源);新增 §7.20 记这三个坑与推论(产物是 gitignore 的, "源码修好"≠"用户手上那个包修好")。 ## 验证 `npm test` 退出码 0(13 个判据文件全绿 + vitest 263 passed); `hvigorw assembleHap` BUILD SUCCESSFUL;`npm run build` + electron-builder 均 exit 0。
This commit is contained in:
@ -1,6 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { AGGREGATE_ID, isUsableAccount, useAccountStore } from './accountStore';
|
||||
/*
|
||||
* ⚠️ `AGGREGATE_ID` / `isUsableAccount` 住在 `lib/accounts.ts`(不在 accountStore 里,
|
||||
* accountStore 只是引用了它们)。我第一版从 `./accountStore` 导入,**vitest 全绿、生产构建直接失败**
|
||||
* (rollup: "AGGREGATE_ID is not exported by src/stores/accountStore.ts")——
|
||||
* 测试运行时对"缺的具名导出"是宽容的(拿到 undefined),只有打包器会报。
|
||||
* 教训:**生产构建是一道独立的门**,测试通过不等于能打包(见 §7.20)。
|
||||
*/
|
||||
import { AGGREGATE_ID, isUsableAccount } from '../lib/accounts';
|
||||
import { useAccountStore } from './accountStore';
|
||||
import { DEFAULT_BLUR, DEFAULT_DIM } from '../lib/appearanceDefaults';
|
||||
|
||||
/**
|
||||
|
||||
@ -92,15 +92,40 @@ test('★ 缓存键按账号分:两端的键都带账号,且都不许退回
|
||||
// 键函数:必须把 accountId 拼进去(按块取函数体,不看调用点 —— "配对/解析"那条)
|
||||
const at = store.indexOf('export function storageKey(');
|
||||
assert.ok(at > 0, '要有 storageKey 函数');
|
||||
/*
|
||||
* ⚠️ 只取**函数体**(第一个 `{` 到配对的 `}`),不能把签名一起算进来 ——
|
||||
* 我第一版就是从 `export function` 开始切、然后在整段里找 `accountId … STORAGE_KEY_PREFIX`,
|
||||
* 结果**变异测试抓出了这个判据是假的**:把实现改成 `return STORAGE_KEY_PREFIX;`(退回全局键)
|
||||
* 时它依然绿,因为参数表里的 `accountId` 已经满足了那个正则。
|
||||
* 这正是本仓规范第 1 条(判结构要配对/解析,不要靠邻接与窗口)—— 我写规范时没想到
|
||||
* 它的适用对象也包括"这个函数自己有没有把账号拼进去"。
|
||||
*/
|
||||
const bodyStart = store.indexOf('{', at);
|
||||
let depth = 0;
|
||||
let end = at;
|
||||
for (let i = store.indexOf('{', at); i < store.length; i++) {
|
||||
for (let i = bodyStart; i < store.length; i++) {
|
||||
if (store[i] === '{') depth++;
|
||||
else if (store[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
const body = store.slice(at, end + 1);
|
||||
assert.match(body, /STORAGE_KEY_PREFIX[\s\S]*accountId|accountId[\s\S]*STORAGE_KEY_PREFIX/,
|
||||
`取键函数必须把账号拼进键里(现在:${body.replace(/\s+/g, ' ')})`);
|
||||
const body = store.slice(bodyStart + 1, end);
|
||||
/*
|
||||
* 仍然不能用"两个词离得近"来判:第二次变异(`return accountId ? STORAGE_KEY_PREFIX : STORAGE_KEY_PREFIX;`
|
||||
* ——提了一下 accountId 但根本没用它)又把松正则骗过去了。
|
||||
* 要求**同一个表达式里**既出现常量又插值/拼接账号:模板字面量 `` `a${b}` `` 或 `a + b`。
|
||||
* 这样"提到"与"用上"就分开了。
|
||||
*/
|
||||
const exprs = [
|
||||
...[...body.matchAll(/`[^`]*`/g)].map(m => m[0]),
|
||||
...[...body.matchAll(/[^;\n{}]*\+[^;\n{}]*/g)].map(m => m[0])
|
||||
];
|
||||
const joins = exprs.filter(e => e.includes('STORAGE_KEY_PREFIX') && e.includes('accountId'));
|
||||
assert.ok(joins.length > 0,
|
||||
`取键的函数体里必须**真的**把账号拼进键(模板插值或 + 拼接)。现在函数体是:${body.replace(/\s+/g, ' ').trim()}`);
|
||||
/*
|
||||
* 行为面由 vitest 兜底(`test/stores/background.test.ts` 直接断言
|
||||
* storageKey('acct-a') === 'agentmail.background.acct-a' 且两个账号不相等)——
|
||||
* 静态判据只负责"形状",值对不对由真跑一遍的函数说了算。
|
||||
*/
|
||||
/*
|
||||
* 不许写死键名:写入的键只能是 `storageKey()`,或由它算出来的变量(迁移时写 `key`)。
|
||||
* 我第一版写成"只允许字面量 setItem(storageKey(" —— 结果把迁移那次合法写入
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@ -86,3 +86,44 @@ test('★ 版本库里不得跟踪缓存/构建产物(.tmp、node_modules、di
|
||||
// 那个占位文件必须还在:它是 go:embed 落点存在的唯一理由,删了 Go 侧就编不过
|
||||
assert.ok(tracked.includes(EMBED_PLACEHOLDER), 'go:embed 的占位文件丢了 —— 没有它 static/ 目录不存在,Go 侧编不过');
|
||||
});
|
||||
|
||||
test('★ dist 必须比 src 新(前端改了没重新构建 —— 套件照样全绿,只有这道门会红)', () => {
|
||||
/*
|
||||
* 这条是踩出来的(2026-09-14 接手 pi 的 WebUI 开项时):
|
||||
* 我把 `npm run build` 串在管道里(`npm run build 2>&1 | tail -4 && electron-builder …`),
|
||||
* **构建失败被管道吞了**(pipeline 的退出码是 tail 的),于是 electron-builder
|
||||
* 拿旧的 dist 打了一个新包 —— 而所有判据都是绿的:
|
||||
* - vitest 绿:测试运行时对"缺的具名导出"很宽容(拿到 undefined),只有打包器会报;
|
||||
* - packaging 绿:它比的是"dist vs 安装包",两边都是旧的,自然一致。
|
||||
* 结果就是"代码改了、产物没改",而没有任何一条判据看得见。
|
||||
*
|
||||
* 这里直接比时间戳:dist 的入口必须不早于 src 下最新的文件。
|
||||
*/
|
||||
const distIndex = join(ROOT, 'client/electron/dist/index.html');
|
||||
if (!existsSync(distIndex)) {
|
||||
console.log('(没有 dist —— 先 npm run build 才验得到这条)');
|
||||
return;
|
||||
}
|
||||
const distAt = statSync(distIndex).mtimeMs;
|
||||
// 构建过程自己会写 src/background-takeover.generated.css(在 dist 之前),不算"源码改动"
|
||||
const newest = { at: 0, file: '' };
|
||||
const walk = dir => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p);
|
||||
else if (e.name !== 'background-takeover.generated.css') {
|
||||
const at = statSync(p).mtimeMs;
|
||||
if (at > newest.at) { newest.at = at; newest.file = p; }
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(join(ROOT, 'client/electron/src'));
|
||||
assert.ok(newest.file, 'src 下应当有文件');
|
||||
assert.ok(
|
||||
distAt >= newest.at,
|
||||
`前端源码比 dist 新(改了没重新构建):\n` +
|
||||
` src 最新:${newest.file.replace(ROOT + '/', '')}\n` +
|
||||
` dist:${distIndex.replace(ROOT + '/', '')}\n` +
|
||||
` 重构建:cd client/electron && npm run build(注意别把它的退出码丢在管道里)`
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user