/* pi 桥的身份自检判据(2026-10-05)。 ★ 起因(一份 bug 报告里的三条,我只认了其中一条真): 报告称「扩展 key 缓存在模块加载期,换 key 后不自知,仍以旧身份发信」。 我先验证了服务端行为,发现**服务端早就内置了这道闸**: POST /api/v1/agent/register {"name":"pi"} + dsh 的 key → {"error":"该密钥已绑定到 Agent \"dsh\",不能用于注册 \"pi\""} 也就是说「这把 key 到底是谁的」服务端能权威回答。 而旧代码把注册失败**当普通错误打印一行就继续跑** —— 于是继续以错误身份发信/收信/上报心跳,且日志里读起来一切正常。 ★ 顺带纠正报告里的另一条:它说「服务端会用 200 + {error} 返回业务失败」。 实测穷举了所有失败路径(403/400/401/404),**没有一条 200 带 error**; `Error(w, status, msg)` 的 status 是必填参数,全仓无 `Error(w, 200, …)`。 所以那不是「假成功」的成因。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const src = readFileSync(join(here, '../src/index.mjs'), 'utf8'); test('★ 身份绑定冲突必须停下,不能只记日志继续跑', () => { // 服务端已经给了确定答复,桥必须据此中止 —— // 否则它会继续以错误身份发信,而日志里看不出来。 assert.match(src, /\/已绑定到 Agent\/\.test\(why\)/, '★ 没有识别「身份绑定冲突」—— 注册失败被当普通错误处理,继续以错误身份运行'); assert.match(src, /throw new Error\(`身份冲突:\$\{why\}`\)/, '★ 识别到了却没中止 —— 冲突时仍继续发信/收信/心跳'); }); test('只对绑定冲突停,不把可恢复错误也停掉', () => { // 密钥无效 / 网络不通属于可恢复的:停掉会连带 SSE 与心跳, // 而那两样本就该在重试后自愈。 const m = src.match(/if \(\/已绑定到 Agent\/\.test\(why\)\) \{([\s\S]*?)\n {4}\}/); assert.ok(m, '找不到绑定冲突分支'); assert.doesNotMatch(m[1], /密钥无效/, '★ 冲突分支里不要混入「密钥无效」的处置 —— 那是另一类可恢复错误'); assert.match(src, /注册失败: \$\{why\}/, '注册失败仍要记日志(停之前得让人看见原因)'); }); test('冲突时要说清怎么查、怎么改', () => { assert.match(src, /agent_keys/, '没有告诉用户去哪儿查这把 key 属于谁'); assert.match(src, /重启进程/, '没有提示必须重启(配置在模块加载期读入,改 env 不热生效)'); }); test('启动日志要印出密钥片段,让身份漂移可见', () => { // 原先只印 AGENT_NAME(本地配置里的),而身份实际由密钥决定 ⇒ // 「配置的 name 与密钥归属不一致」在日志里完全看不出来。 assert.match(src, /String\(agentKey\)\.slice\(0, 8\)/, '★ 启动日志没印 key 片段 —— 身份漂移无法一眼对出'); assert.doesNotMatch(src, /\$\{agentKey\}(?!\.slice)\}/, '★ 密钥被原样打进日志 —— 会泄进日志文件'); }); test('身份由密钥决定,不靠 name/secret 头', () => { // 报告里提到「X-Agent-Name 对服务端无效」。这条要钉住: // 身份只来自 Bearer 密钥,所以打印 config 里的名字不足以代表真实身份。 assert.match(src, /密钥认证/, '启动日志应区分密钥认证与 name/secret 两条路径'); });