Files
MailUI4Agents/plugins/opencode-mail-bridge/test/rename-proposal.test.mjs

137 lines
5.7 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.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
isProposableAlias,
appendRenameProposal,
renameProposalNote,
} from '../lib/rename-proposal.js';
// 这一组测试钉住的是「插件拼的标记与服务端正则逐字符对应」。
// 服务端那条正则在 server/internal/handler/rename_proposal.go
// <!--\s*agentmail:rename-session\s+alias="([^"]*)"(?:\s+reason="([^"]*)")?\s*-->
// 拼错不会报错 —— 邮件照常发出,提议凭空消失。
/** 服务端正则的等价实现,用来验证我们拼出来的标记真的能被摘出来。 */
const SERVER_RE =
/<!--\s*agentmail:rename-session\s+alias="([^"]*)"(?:\s+reason="([^"]*)")?\s*-->/s;
test('isProposableAlias: 合法别名', () => {
assert.equal(isProposableAlias('fix-login-leak'), true);
assert.equal(isProposableAlias('修复登录态泄漏'), true, '中文别名合法');
assert.equal(isProposableAlias('v2_migration'), true);
});
test('isProposableAlias: new 是寻址保留字', () => {
// `.new` 是「强制新建会话」的动作,别名叫 new 会让地址无从解释
assert.equal(isProposableAlias('new'), false);
});
test('isProposableAlias: 拒绝与三维地址冲突的字符', () => {
// 这四个字符都会让 name@path.session 的切分产生歧义
assert.equal(isProposableAlias('a.b'), false, '. 是 session 位分隔符');
assert.equal(isProposableAlias('a/b'), false, '/ 出现在 path 位');
assert.equal(isProposableAlias('a@b'), false, '@ 是 name/path 分隔符');
assert.equal(isProposableAlias('a b'), false, '空白');
assert.equal(isProposableAlias('a\tb'), false, '制表符也算空白');
});
test('isProposableAlias: 拒绝双引号', () => {
// 双引号是标记自身的定界符,含它会把标记截断成非法形式
assert.equal(isProposableAlias('say"hi'), false);
});
test('isProposableAlias: 空与空白视为没提', () => {
assert.equal(isProposableAlias(''), false);
assert.equal(isProposableAlias(' '), false);
assert.equal(isProposableAlias(undefined), false);
assert.equal(isProposableAlias(null), false);
});
test('isProposableAlias: 超过 128 字节按字节算', () => {
// 服务端是 VARCHAR(128)。中文一个字 3 字节43 字 = 129 字节
assert.equal(isProposableAlias('a'.repeat(128)), true);
assert.equal(isProposableAlias('a'.repeat(129)), false);
assert.equal(isProposableAlias('汉'.repeat(42)), true, '126 字节');
assert.equal(isProposableAlias('汉'.repeat(43)), false, '129 字节');
});
test('标记能被服务端正则摘出来', () => {
const { body, proposed } = appendRenameProposal('已定位到问题。', 'fix-login-leak', '登录态泄漏');
assert.equal(proposed, true);
const m = SERVER_RE.exec(body);
assert.ok(m, '服务端正则必须匹配得上');
assert.equal(m[1], 'fix-login-leak');
assert.equal(m[2], '登录态泄漏');
});
test('没有理由时整个 reason 属性都不写', () => {
// 写成 reason="" 会让服务端存一个空理由,界面提示条就少了那句解释
const { body } = appendRenameProposal('正文', 'fix-leak');
assert.doesNotMatch(body, /reason=/);
const m = SERVER_RE.exec(body);
assert.equal(m[1], 'fix-leak');
assert.equal(m[2], undefined);
});
test('理由里的双引号被去掉而不是转义', () => {
// HTML 注释里没有转义机制,留着会截断标记
const { body } = appendRenameProposal('正文', 'fix-leak', '他说"这是泄漏"');
const m = SERVER_RE.exec(body);
assert.ok(m);
assert.equal(m[2], '他说这是泄漏');
});
test('原正文完整保留在标记之前', () => {
const original = '第一行\n\n第二行';
const { body } = appendRenameProposal(original, 'fix-leak');
assert.ok(body.startsWith(original), '正文不得被改写');
});
test('别名不合法时原样返回,不追加标记', () => {
const { body, proposed } = appendRenameProposal('正文', 'a.b');
assert.equal(proposed, false);
assert.equal(body, '正文');
assert.doesNotMatch(body, /agentmail:rename-session/);
});
test('不给别名时正文完全不变', () => {
const { body, proposed } = appendRenameProposal('正文', '');
assert.equal(proposed, false);
assert.equal(body, '正文');
});
test('renameProposalNote: 用服务端回的别名,不是本地提议的', () => {
// 服务端跑 normalizeAlias非法字符换 -、new 变 session-new、超长截断。
// 回显本地值会让模型记住一个不存在的名字,之后拿它寻址就 404。
const note = renameProposalNote('fix-login-leak', 'fix.login.leak', true);
assert.match(note, /fix-login-leak/);
assert.match(note, /规范化/, '要告诉模型名字被改写过');
assert.match(note, /确认/);
assert.match(note, /原别名/, '要明确说在那之前用哪个');
});
test('renameProposalNote: 服务端别名与提议一致时不提规范化', () => {
const note = renameProposalNote('fix-leak', 'fix-leak', true);
assert.match(note, /fix-leak/);
assert.doesNotMatch(note, /规范化/);
});
test('renameProposalNote: 本地判非法时说清为什么', () => {
const note = renameProposalNote('', 'a.b', false);
assert.match(note, /未提交/);
assert.match(note, /a\.b/);
});
test('renameProposalNote: 标记发出但服务端没接受', () => {
// 本地校验比服务端宽的情况(例如服务端加了新约束)——
// 不能沉默,否则模型以为提议成功了
const note = renameProposalNote('', 'somealias', true);
assert.match(note, /未被服务端接受/);
assert.match(note, /somealias/);
});
test('renameProposalNote: 没提议时不产生噪音', () => {
assert.equal(renameProposalNote('', '', false), '');
assert.equal(renameProposalNote(undefined, undefined, false), '');
});