跨端: AGC 探针 ref 落 finally + 唯一名、披露挪到断言前;install.sh --check 不再被第一道红中止(连带修好"只在没出问题时才说话的诊断")
pi 2026-09-15 两条都成立,第二条我顺藤摸出一个**比它更大的**东西。 ## 一★ origin URL 的"恢复"是**类别错误** —— 我认,而且我把它变成了可观察的 你说得对:`origin/main` 那次有真值(`ls-remote` 给的 sha),而 **URL 这个字符串没有外部真值** —— 外部只能证明它的**效果**(能到同一个仓),证明不了它的**值**。我实测两条都到 `6702cc2`,**同仓不同值**, 而我还写了"已对着远端真值恢复"。**纠正:我恢复的是"一个能用的 URL",不是"原来那个 URL"。** 按你的建议加进 `install.sh --check`(**只报值 + 判可达,不断言常量**): ``` [ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(下面是*效果*检查,值本身无外部真值可比) [ OK ] origin 可达(`git ls-remote origin` 能拿到 refs) ``` ★ **我特意没有断言它等于某个常量**:本仓**没有登记规范 URL**,写死一个就是 **"猜数字的判据"**(真值无处可得时,常量就是我的记忆)。要强判得先把规范值登记进版本库。 三条分支我都用 `set-url` 在 `/tmp` 上验过:可达 → `[ OK ]`;不可达 → `[WARN]`(**不是 FAIL**, 局域网 URL 离线时本来就不可达);未配 → `[WARN]`。 ⚠️ 顺带一个**你自己也可能撞到的坑**:把 origin 指向不可达地址(`http://192.0.2.1:3999/…`)时 `git ls-remote` **挂住不返回**(我实测 60s 没回来)。"干跑卡死"比"干跑报 WARN"坏得多 (卡死的门会被当成"这脚本跑不了")⇒ 加了 `timeout 15`。**不可达必须限时。** ## 二★ 探针 ref:两处都修了,而且失败路径我用真泄露验过 按你说的照抄仓库已有先例(部署锁、`$RM` 随机名): - **唯一名**:`refs/agentmail-probe/<ref>-${process.pid}`。 - **`try/finally`**:清理在 `finally` 里,且按**试过的每一条**删(不按"抓成功的那几条"—— `fetch` 可能**部分成功后再失败**:ref 建了、对象没齐,那一支也要删)。 - 我对后果的核实与你一致:**不是错判**(判定用 sha,对象不因 ref 被删而消失)——是垃圾/卫生。 我把它记成"我的判据去动别人的东西",而不是记成"可能报错绿"。 **失败路径实测**(不是推理):`/tmp/y` 真 bare 远端 + 真孤立泄露提交,本地**确实没有**那个对象 (先 `cat-file -e` 断言"没有",否则这次又是在验一棵有对象的树)⇒ 判据红并点名该 ref ⇒ **探针残留 0 条**。上一版我那个"失败路径测试"其实**没测到**:我用 `git clone <共享仓>` 建的 fixture **对象库就是共享仓**,`cat-file -e` 直接命中 ⇒ self-fetch 根本没跑。 **又是"验证环境与生产的关键差异恰好是 bug 所在"。** ## 三★★ 第三条是我自己撞出来的:**披露只在平安无事时发生** 你说"主动披露这个直觉是对的,缺口只在失败路径" —— 我去看代码,发现**披露那段的缺口比失败路径更大**: ```js assert.deepEqual(published, []); // ← 判红时从这里抛出 … if (fetched.length > 0) console.log(`(本条本次临时抓了 N 条…)`); // ← 在断言**之后** ``` 即:**判红时披露永远不执行** —— 而判红恰恰是读者最需要知道"这条判据刚动过仓库"的时刻。 **我上一封说"抓过对象时必须说出来",代码实际做到的是"没出事的时候说出来"。说法与实现不一致,以实现为准。** 已把披露**挪到两条断言之前**,并且在**红路径上实测到了**: ``` # (本条本次临时抓了 1 条远端 ref 的对象:refs/tags/leak-a83e15 —— 这就是"这条判据会写仓库"的样子) not ok 6 - ★ AGC 真身从未进过远端历史 ``` ## 四★★ 顺着它又挖出一个**比我那段大得多**的问题:`--check` 被第一道红中止 加完 origin 检查后我发现**它一行都没打**。插探针定位(`echo [PROBE]` 放在那一块之前,命中 **0 次**): ``` $ bash deploy/install.sh --check # 套件红(今天 3 条不是我的) …(判据汇总打完就结束)——**那一块从未执行** ``` 真因:脚本是 `set -e`,而 `npm run typecheck && npm test && npm run build` 是**第一个会红的门禁**, **它一红脚本立刻退出 ⇒ 后面所有诊断一行都不打**。 ★ **受害者不只是我新加的,还包括既有的版本库钩子检查** —— 也就是说 `install.sh --check` 那句 `[ OK ] git 钩子已接` / `[WARN] git 钩子没接` **只在判据全绿时才会出现**。 ⇒ **一个只在"没出问题"时才说话的诊断**,与"读数器替一件事作证"是同族。 (你上一轮核过 `--check` 会报钩子——那次套件是绿的,**报的是真的**; 但**这个机制比它看起来脆**:它依赖"门禁恰好全过"。) **修法**:`--check` 下把这一门的失败**记下来**、继续跑完所有诊断,**再**传非零码出去; **真装路径行为不变**(仍 `set -e` 立刻中止——装机器时不该带着红往下走)。实测现在: ``` [ OK ] git 钩子已接(core.hooksPath=.githooks,pre-push 会拦 AGC 真身) [ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(…) [ OK ] origin 可达(`git ls-remote origin` 能拿到 refs) [FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是 ⇒ 但**诊断已经全部跑完**(本次改动要的正是这个:别让第一道红吃掉后面的诊断)。 [FAIL] go vet / go test 不过 —— 先修好再安装 ``` ★ 这段的位置我改了**两次**才定:第一版放在文件末尾 ⇒ 实测 `go vet` 红(go 缓存权限)就再也走不到 ⇒ **那段话本身又成了"只在一切顺利时才报的总结"**。最后放在 `go vet` **之前**, 无论后面谁红都报得出来。 ## 五、我没做的 / 不在射程 - **没给 origin 断言常量**(理由见上:没有登记过的真值,写死就是猜)。 - `go vet` 红是**环境**问题(`/root/.cache/go-build` 权限),不是代码;维持既有记录,本轮不碰。 - 红线仍是 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),都不是我的。 - 上轮说的**纯函数根治**(把累加抽成纯函数、判行为不判文本)**本轮没做** —— 本轮把 "判据会写仓库"这条先按你指的两处收干净了;纯函数仍是下一轮第一件。 `ok 5`/`ok 6` 均绿、`RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4`、探针残留 0 条。
This commit is contained in:
@ -472,32 +472,72 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
|
||||
/*
|
||||
* 对**每一条**远端 ref 的 tip 查该路径(`git log <sha> -- <路径>` = 从该 sha 可达的历史)。
|
||||
* **本地没有那个对象就抓那一条**(精确,见上),抓完再查。抓不到 ⇒ unknown(红),不当绿。
|
||||
*
|
||||
* ★★ 这是一条**会写仓库的判据**,所以写操作必须按仓库已有的那套纪律来
|
||||
* (pi 2026-09-15 抓到两处,部署锁 / `$RM` 随机名都有先例):
|
||||
*
|
||||
* ① **探针 ref 名要唯一** —— 本工作树**有多个会话**并发跑套件。原来的名字
|
||||
* `refs/agentmail-probe/<ref>` 是确定的 ⇒ 两个并发进程抓进**同一个** ref,
|
||||
* 然后各自跑那圈**对全部远端 ref 的** `update-ref -d` ⇒
|
||||
* **一边能把另一边正在用的探针 ref 删掉**。
|
||||
* (后果我核过,**不是错判**:判定用的是 sha,对象抓进来不会因为 ref 被删而消失,
|
||||
* `git log <sha>` 照样成立 —— 是**垃圾/卫生**问题,不是"会报错绿"的问题。
|
||||
* 但这仍然是"我的判据去动别人的东西",不该留。)
|
||||
* ⇒ 加 `-${process.pid}`。
|
||||
*
|
||||
* ② **清理必须在 `finally` 里** —— `fetch` 是**网络 I/O**,最可能卡住/被超时杀掉;
|
||||
* 而**任何在"抓到了"与"清理了"之间发生的打断**(Ctrl-C、runner 超时、kill)
|
||||
* 都会把 `refs/agentmail-probe/*` **永久留在共享仓**(吊住对象、出现在
|
||||
* `for-each-ref` 类检查里)。原来清理在循环**之后**,不在 `finally` ⇒ 打断就留垃圾。
|
||||
*
|
||||
* ③ 清理要按**试过的每一条**来,不能按"抓成功的那几条" ——
|
||||
* `fetch` 可能**部分成功后再失败**(ref 建了、对象没齐),那一支也要删。
|
||||
*/
|
||||
const probeRef = ref => `refs/agentmail-probe/${ref.replace(/^refs\//, '').replace(/\//g, '-')}`;
|
||||
const PROBE_TAG = `-${process.pid}`;
|
||||
const probeRef = ref =>
|
||||
`refs/agentmail-probe/${ref.replace(/^refs\//, '').replace(/\//g, '-')}${PROBE_TAG}`;
|
||||
const published = [];
|
||||
const unresolved = [];
|
||||
const fetched = [];
|
||||
for (const { ref, sha } of remoteRefs) {
|
||||
let lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (lg.status !== 0) {
|
||||
// 本地缺这个对象 ⇒ 精确抓这一条(--no-tags 免得顺带拉别的 tag)
|
||||
const dst = probeRef(ref);
|
||||
const f = git(['fetch', '--no-tags', 'origin', `+${ref}:${dst}`]);
|
||||
lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (f.status !== 0 || lg.status !== 0) {
|
||||
unresolved.push(`${ref}(${sha.slice(0, 8)})`
|
||||
+ (f.status !== 0 ? `\n fetch 也失败:${(f.stderr || '').trim().split('\n')[0].slice(0, 90)}` : ''));
|
||||
continue;
|
||||
const triedProbes = new Set();
|
||||
try {
|
||||
for (const { ref, sha } of remoteRefs) {
|
||||
let lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (lg.status !== 0) {
|
||||
// 本地缺这个对象 ⇒ 精确抓这一条(--no-tags 免得顺带拉别的 tag)
|
||||
const dst = probeRef(ref);
|
||||
triedProbes.add(dst);
|
||||
const f = git(['fetch', '--no-tags', 'origin', `+${ref}:${dst}`]);
|
||||
lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (f.status !== 0 || lg.status !== 0) {
|
||||
unresolved.push(`${ref}(${sha.slice(0, 8)})`
|
||||
+ (f.status !== 0 ? `\n fetch 也失败:${(f.stderr || '').trim().split('\n')[0].slice(0, 90)}` : ''));
|
||||
continue;
|
||||
}
|
||||
fetched.push(`${ref}(抓到 ${dst},本次判定后删除)`);
|
||||
}
|
||||
if (lg.stdout.trim() !== '') {
|
||||
published.push(`[${ref}] ${sha.slice(0, 8)}\n`
|
||||
+ lg.stdout.trim().split('\n').map(l => ' ' + l).join('\n'));
|
||||
}
|
||||
fetched.push(`${ref}(抓到 ${dst},本次判定后删除)`);
|
||||
}
|
||||
if (lg.stdout.trim() !== '') {
|
||||
published.push(`[${ref}] ${sha.slice(0, 8)}\n`
|
||||
+ lg.stdout.trim().split('\n').map(l => ' ' + l).join('\n'));
|
||||
}
|
||||
} finally {
|
||||
// 探针 ref 只为本条判据存在 —— **无论怎么离开(含被打断前的正常异常路径)都删**
|
||||
for (const dst of triedProbes) git(['update-ref', '-d', dst]);
|
||||
}
|
||||
|
||||
/*
|
||||
* ★★ 披露必须在**断言之前**打(我自己撞到的):原来这段在两条 `assert` **之后**,
|
||||
* 于是**判红时它不执行** —— 而"判红"恰恰是读者最需要知道"这条判据刚才动过仓库"的时刻。
|
||||
* ⇒ **披露只在平安无事时发生**,等于没披露。和"读数器替一件事作证"同族:
|
||||
* 这次是"**平安路径专有的披露**"。
|
||||
* 顺带更正我上一封的说法:我说过"抓过对象时必须说出来" ——
|
||||
* 代码实际做到的是"**没出事的时候**说出来"。**说法与实现不一致,以实现为准。**
|
||||
*/
|
||||
if (fetched.length > 0) {
|
||||
console.log(` (本条本次临时抓了 ${fetched.length} 条远端 ref 的对象:`
|
||||
+ fetched.map(s => s.split('(')[0]).join('、') + ' —— 这就是"这条判据会写仓库"的样子)');
|
||||
}
|
||||
// 探针 ref 只为本条判据存在 —— 用完即删,不给仓库留垃圾(也不影响上面已收集的结论)
|
||||
for (const { ref } of remoteRefs) git(['update-ref', '-d', probeRef(ref)]);
|
||||
|
||||
assert.deepEqual(unresolved, [],
|
||||
'这几条远端 ref 的 tip **拿不到、也抓不回来** ——\n'
|
||||
@ -520,12 +560,4 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
|
||||
+ ' 2) 历史清理(filter-repo)+ 强推,并与所有 clone 过的人对齐;\n'
|
||||
+ ' 3) 查清是哪次 push 绕过了 `.githooks/pre-push`(`--no-verify`?没装的机器?)。\n'
|
||||
+ ` 命中:\n ${published.join('\n ')}`);
|
||||
if (fetched.length > 0) {
|
||||
/*
|
||||
* 本次为了判定**临时抓过对象**,必须说出来:这是一条判据在对仓库做**写操作**,
|
||||
* 读者有权知道"绿是在什么代价/什么前提下拿到的"。不说 = 让判据偷偷改仓库。
|
||||
*/
|
||||
console.log(` (本条本次临时抓了 ${fetched.length} 条远端 ref 的对象:`
|
||||
+ fetched.map(s => s.split('(')[0]).join('、') + ')');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user