Files
MailUI4Agents/plugins/pi-mail-bridge/test/relay-key.test.mjs
JianFeeeee 13fcb00acc feat: relay-key 共用模块(三桥 + homeagent)
Sha256 clamp relay_key 过 160 字节上限,避免服务端 400
被 worker 当暂时失败让位,导致邮件驱动会话无本地 UI 静默挂死。

新增 relay_key.go / relay-key.js + 15 个纯函数测试。
2026-09-06 15:16:34 +08:00

195 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* lib/relay-key.js 的测试 —— 四个平台逐字节共用。
*
* 事故背景生产实测pi 会话里 bash 的 relay_key 突然超过服务端 160 字节
* 列宽,返回 400。真实会话文件里 toolCallId 有两种形态:
* toolu_bdrk_01F6roEBHa8nic1mYiyLgNWK 35 字节
* toolu_bdrk_01FsWUWhEs4arnEWo44gqzLC~sig1:CAISoQIK… 437 ~ 13601 字节
* 启用 extended thinking 时 Bedrock 把思考签名拼进了 toolCallId。
*
* 更严重的是那次 400 被归入「暂时失败 → 让位给本地决策」,而邮件驱动的
* worker 没有 TUI —— 那次 bash 没有任何人批准就执行了。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import {
RELAY_KEY_MAX_BYTES,
byteLength,
truncateToBytes,
clampRelayKey,
isPermanentFailure,
} from '../lib/relay-key.js';
// ─── byteLength ───
test('byteLength 算的是 UTF-8 字节而不是字符数', () => {
assert.equal(byteLength('abc'), 3);
assert.equal(byteLength('中文'), 6); // 每个 3 字节
assert.equal(byteLength(''), 0);
assert.equal(byteLength(null), 0);
assert.equal(byteLength(undefined), 0);
});
// ─── truncateToBytes ───
test('未超限时原样返回', () => {
assert.equal(truncateToBytes('abcdef', 10), 'abcdef');
assert.equal(truncateToBytes('abcdef', 6), 'abcdef');
});
test('ASCII 按字节精确截断', () => {
assert.equal(truncateToBytes('abcdef', 3), 'abc');
});
test('不切出半个多字节字符', () => {
// '中文' = 6 字节。上限 4 时不能切出 '中' + 半个 '文'
const out = truncateToBytes('中文', 4);
assert.equal(out, '中');
assert.equal(byteLength(out) <= 4, true);
// 结果必须能无损往返(有半个字符时会变成 U+FFFD
assert.equal(out.includes('\uFFFD'), false);
});
test('截断结果的字节数永不超上限(扫一遍长度)', () => {
const s = '会话abc标识def中文gh';
for (let limit = 0; limit <= byteLength(s) + 2; limit++) {
const out = truncateToBytes(s, limit);
assert.equal(byteLength(out) <= limit, true, `limit=${limit} 时超了`);
assert.equal(out.includes('\uFFFD'), false, `limit=${limit} 时切出了半个字符`);
}
});
test('上限 0 或负数返回空串', () => {
assert.equal(truncateToBytes('abc', 0), '');
assert.equal(truncateToBytes('abc', -5), '');
});
// ─── clampRelayKey ───
test('正常长度的键原样返回(不能改写已合规的键)', () => {
// 生产上真实的 pi 键36 字节会话 id + ':' + 35 字节 toolCallId = 72
const key = '01a05a5e-8abb-7bf4-bc87-47eadae619a8:toolu_bdrk_01CJevE1rw69DyVWSJv3n3eA';
assert.equal(byteLength(key) <= RELAY_KEY_MAX_BYTES, true);
assert.equal(clampRelayKey(key), key);
});
test('恰好等于上限时原样返回(边界不能差一)', () => {
const key = 'k'.repeat(RELAY_KEY_MAX_BYTES);
assert.equal(clampRelayKey(key), key);
});
test('超一个字节就收敛', () => {
const key = 'k'.repeat(RELAY_KEY_MAX_BYTES + 1);
const out = clampRelayKey(key);
assert.notEqual(out, key);
assert.equal(byteLength(out) <= RELAY_KEY_MAX_BYTES, true);
});
test('收敛后一定不超上限(用真实的带签名 toolCallId 长度)', () => {
// 生产实测 437 ~ 13601 字节都出现过
for (const n of [437, 1000, 5493, 13601]) {
const key = `01a05a5e-8abb-7bf4-bc87-47eadae619a8:toolu_bdrk_01X~sig1:${'A'.repeat(n)}`;
const out = clampRelayKey(key);
assert.equal(byteLength(out) <= RELAY_KEY_MAX_BYTES, true, `n=${n} 时超了`);
}
});
test('同一输入永远得到同一输出(幂等键的根本要求)', () => {
const key = `sess:${'x'.repeat(500)}`;
assert.equal(clampRelayKey(key), clampRelayKey(key));
});
test('不同输入不撞键 —— 这正是不能直接截断的理由', () => {
// 两个键前 160 字节完全相同,只有尾部不同。
// 直接截断会让它们变成同一个键,第二次询问被服务端当重复请求丢掉。
const common = 'a'.repeat(300);
const k1 = `${common}:call-1`;
const k2 = `${common}:call-2`;
assert.notEqual(clampRelayKey(k1), clampRelayKey(k2));
});
test('收敛结果保留可读前缀(日志里还能 grep 出会话)', () => {
const sid = '01a05a5e-8abb-7bf4-bc87-47eadae619a8';
const out = clampRelayKey(`${sid}:toolu_bdrk_01X~sig1:${'A'.repeat(900)}`);
assert.equal(out.startsWith(sid), true);
assert.match(out, /:sha256:[0-9a-f]{64}$/);
});
test('哈希是原始键的完整 sha256不是截断后的', () => {
const key = `sess:${'y'.repeat(400)}`;
const expect = createHash('sha256').update(key, 'utf8').digest('hex');
assert.equal(clampRelayKey(key).endsWith(`:sha256:${expect}`), true);
});
test('含中文的超长键不切出半个字符', () => {
const key = `会话标识:${'中'.repeat(300)}`;
const out = clampRelayKey(key);
assert.equal(byteLength(out) <= RELAY_KEY_MAX_BYTES, true);
assert.equal(out.includes('\uFFFD'), false);
});
test('上限小到装不下哈希时退化为截断哈希(仍然确定)', () => {
const key = 'z'.repeat(500);
const out = clampRelayKey(key, 20);
assert.equal(byteLength(out) <= 20, true);
assert.equal(out, clampRelayKey(key, 20));
});
test('空键与 null 不炸', () => {
assert.equal(clampRelayKey(''), '');
assert.equal(clampRelayKey(null), '');
assert.equal(clampRelayKey(undefined), '');
});
// ─── isPermanentFailure ───
test('400 是永久失败 —— 事故的核心(原来被当暂时失败让位)', () => {
assert.equal(isPermanentFailure({ status: 400 }), true);
});
test('409 是永久失败(这条链上没有人类,永远不会有人点头)', () => {
assert.equal(isPermanentFailure({ status: 409 }), true);
});
test('401 是永久失败:密钥无效要人去后台登记,不是等一等就好', () => {
// 本会话实测opencode 拿着已撤销的密钥重试了 18 小时2690 次 401
assert.equal(isPermanentFailure({ status: 401 }), true);
});
test('403 / 404 / 422 都是永久失败', () => {
for (const s of [403, 404, 422]) {
assert.equal(isPermanentFailure({ status: s }), true, `${s} 应当是永久`);
}
});
test('408 与 429 是暂时失败(超时与限流等一会儿真的可能成功)', () => {
assert.equal(isPermanentFailure({ status: 408 }), false);
assert.equal(isPermanentFailure({ status: 429 }), false);
});
test('5xx 是暂时失败(服务端的问题)', () => {
for (const s of [500, 502, 503, 504]) {
assert.equal(isPermanentFailure({ status: s }), false, `${s} 应当是暂时`);
}
});
test('没有 status 的错误按暂时处理网络层DNS / 连接被拒)', () => {
assert.equal(isPermanentFailure(new Error('fetch failed')), false);
assert.equal(isPermanentFailure({}), false);
assert.equal(isPermanentFailure(null), false);
assert.equal(isPermanentFailure(undefined), false);
});
test('status 是字符串时也能判HTTP 客户端可能挂上字符串)', () => {
assert.equal(isPermanentFailure({ status: '400' }), true);
assert.equal(isPermanentFailure({ status: '503' }), false);
});
test('2xx / 3xx 不算永久失败(本不该走到这里,但不能误判成永久)', () => {
assert.equal(isPermanentFailure({ status: 200 }), false);
assert.equal(isPermanentFailure({ status: 302 }), false);
});