update
This commit is contained in:
182
test/echo-suppress.test.js
Normal file
182
test/echo-suppress.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 回声抑制状态机(lib/echo-suppress.js)—— 测的是 main.js 实际引用的本体。
|
||||
*
|
||||
* 这份逻辑以前内联在 main.js 里,旧测试(poll-suppress.test.js)克隆了一份
|
||||
* 状态机自己测自己:main.js 改坏了测试照样绿,而这是丢数据级的逻辑
|
||||
* (吞错一次 = 用户的一次真实复制静默消失)。现在抽到 lib,测本体。
|
||||
*
|
||||
* 每个用例对应一条真实踩坑规则,注释里写了放掉会发生什么。
|
||||
*/
|
||||
const assert = require('node:assert');
|
||||
const { createEchoSuppress } = require('../lib/echo-suppress.js');
|
||||
|
||||
let passed = 0;
|
||||
function check(name, fn) {
|
||||
try { fn(); console.log(' ok -', name); passed++; }
|
||||
catch (e) { console.error(' FAIL -', name, '\n', e.message); process.exitCode = 1; }
|
||||
}
|
||||
|
||||
check('启动采样:基线内容第一次见到不算新', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.seed('已经在剪贴板里的旧内容');
|
||||
assert.strictEqual(s.observe('已经在剪贴板里的旧内容'), 'unchanged');
|
||||
// 放开这条:每次重启都会把剪贴板里的旧内容重复入库一次
|
||||
assert.strictEqual(s.observe('外部新复制'), 'new');
|
||||
});
|
||||
|
||||
check('自己写进去的回声:下一 tick 不会把它当新内容', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A'); // pending=A,同时基线预置为 A
|
||||
// 读到的回声与预置基线一致 → 'unchanged'(同样不入库)。
|
||||
// suppressed 分支出现在基线与 pending 分叉之后(见下条)。
|
||||
assert.strictEqual(s.observe('A'), 'unchanged');
|
||||
assert.strictEqual(s.observe('A'), 'unchanged');
|
||||
});
|
||||
|
||||
check('suppressed 分支:基线与 pending 分叉后,匹配 pending 的内容被吞', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A');
|
||||
// 外部中途清空剪贴板:基线变 '',pending 不被消化
|
||||
assert.strictEqual(s.observe(''), 'empty');
|
||||
// 此后读到与 pending 一致的内容 → suppressed(设计取舍见 lib 头注释:
|
||||
// 宁可漏掉"清空后外部再复制同内容",也不让回声重复入库)
|
||||
assert.strictEqual(s.observe('A'), 'suppressed');
|
||||
});
|
||||
|
||||
check('回声被外部复制顶掉:外部那次必须入库(占位 flag 实现会在这里丢数据)', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A');
|
||||
// 两次 tick 之间用户在外面复制了 B,tick 直接读到 B
|
||||
assert.strictEqual(s.observe('B'), 'new', '外部复制 B 不能被回声标记吞掉');
|
||||
});
|
||||
|
||||
check('不匹配的新内容消化 pending:之后相同内容的外部复制不被误伤', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A');
|
||||
assert.strictEqual(s.observe('B'), 'new');
|
||||
// 用户又复制回 A(外部)—— pending 已被 B 消化,A 正常入库
|
||||
// 放开这条:点复制 A → 外部 B → 外部再复制 A,第二个 A 被静默吞掉
|
||||
assert.strictEqual(s.observe('A'), 'new');
|
||||
});
|
||||
|
||||
check('剪贴板被清空不消化 pending:清空后复制同内容仍被识别为回声', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A');
|
||||
assert.strictEqual(s.observe(''), 'empty');
|
||||
// 外部中途清空剪贴板不该让回声标记失效,否则紧接着读到 A 会重复入库
|
||||
assert.strictEqual(s.observe('A'), 'suppressed');
|
||||
});
|
||||
|
||||
check('基线已是空串再读空:unchanged 而不是 empty(保住 main.js 的图片分支)', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.seed(''); // 启动时剪贴板没有文本 —— 纯图片场景的常态
|
||||
assert.strictEqual(s.observe(''), 'unchanged');
|
||||
// 放开这条(把 empty 判断挪到 unchanged 之前):文本恒空时每个 tick 都在
|
||||
// 文本分支早退,main.js 永远走不到图片分支 —— 用户复制图片整条静默丢失。
|
||||
assert.strictEqual(s.observe('外部复制'), 'new');
|
||||
});
|
||||
|
||||
check('写入失败清标记:失败后用户的真实复制不被吞', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A');
|
||||
s.clearPending(); // 只清 pending,不动基线
|
||||
assert.strictEqual(s.observe(''), 'empty'); // 让基线与(已清的)pending 分叉
|
||||
// pending 已清 → 读 A 走 'new' 而不是 'suppressed',真实复制不丢
|
||||
assert.strictEqual(s.observe('A'), 'new');
|
||||
});
|
||||
|
||||
check('写入失败回滚基线:失败后真实旧内容不被当新内容重复入库', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.seed('旧内容(已在库里)');
|
||||
const undo = s.markOwnWrite('新内容'); // 模拟点"复制":预置基线 + 记回声
|
||||
undo(); // 模拟 clipboard.writeText 抛错后回滚
|
||||
// 剪贴板上其实还是旧内容,下个 tick 读到它必须判 unchanged。
|
||||
// 放开这条(markOwnWrite 不提供回滚):基线停在'新内容',下个 tick 读到
|
||||
// 真实旧内容判 'new',早已在库里的旧内容被重复插入一条。
|
||||
assert.strictEqual(s.observe('旧内容(已在库里)'), 'unchanged');
|
||||
// 回滚同时清了回声标记:用户之后真的复制了那个内容,照常入库不被吞
|
||||
assert.strictEqual(s.observe('新内容'), 'new');
|
||||
});
|
||||
|
||||
check('基线随每次变化更新:重复读同内容不重复入库', () => {
|
||||
const s = createEchoSuppress();
|
||||
assert.strictEqual(s.observe('X'), 'new');
|
||||
assert.strictEqual(s.observe('X'), 'unchanged');
|
||||
assert.strictEqual(s.observe('X'), 'unchanged');
|
||||
assert.strictEqual(s.observe('Y'), 'new');
|
||||
});
|
||||
|
||||
check('resetBaseline:1×1 空图之后任何真图都算新', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.seed('hash-of-small');
|
||||
s.resetBaseline();
|
||||
assert.strictEqual(s.observe('hash-of-real-image'), 'new');
|
||||
});
|
||||
|
||||
check('abortLastObserve:入库失败回滚后同一条内容能再次入库', () => {
|
||||
// 用户报告的"卡住"场景:observe 判 'new' → 入库失败(SQLite 临时锁、
|
||||
// 磁盘满、字段非法等)→ 下次 tick 读到同内容判 'unchanged' → 永久吞掉。
|
||||
// abortLastObserve 必须撤销 observe 对基线的修改,让下次 tick 再判 'new'。
|
||||
const s = createEchoSuppress();
|
||||
s.seed('之前的内容');
|
||||
assert.strictEqual(s.observe('新内容'), 'new', '第一次观察应判 new');
|
||||
// 模拟 stmt.insertText 抛错:入库失败方在 catch 里调 abortLastObserve
|
||||
s.abortLastObserve();
|
||||
// 下次 tick 读到同样内容 → 必须重新判 'new',让 pollClipboard 再试入库
|
||||
assert.strictEqual(s.observe('新内容'), 'new', 'abort 后应允许重试,不该被吞');
|
||||
// 这次入库成功后基线更新,下次再读 → 'unchanged',正常路径
|
||||
assert.strictEqual(s.observe('新内容'), 'unchanged', '入库成功后基线已建立');
|
||||
});
|
||||
|
||||
check('abortLastObserve:同时清掉 pending(之前 markOwnWrite 留下的回声标记)', () => {
|
||||
// 极小概率但可能:上一次 tick 已经 markOwnWrite(自己写入留了 pending),
|
||||
// 紧接着入库失败 abort —— 这时 pending 是上次的回声标记,不该在 abort
|
||||
// 后继续生效(否则下次 tick 会把外部真实复制当回声吞掉)。
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('A'); // pending='A', last='A'
|
||||
s.observe('B'); // 'new'(pending 被消化)
|
||||
s.markOwnWrite('C'); // pending='C', last='C'
|
||||
s.observe('C'); // 'unchanged'(预置基线命中)
|
||||
// 上面没有 'new' 路径,构造一个会判 'new' 的场景:
|
||||
s.seed('基线');
|
||||
s.markOwnWrite('X'); // pending='X', last='X'
|
||||
s.observe('Y'); // pending 被消化, last='Y', 'new'
|
||||
// 现在状态:last='Y', pending=null。再来一次会判 'new' 的写入:
|
||||
s.markOwnWrite('Z'); // pending='Z', last='Z'
|
||||
assert.strictEqual(s.observe('Z'), 'unchanged');
|
||||
// 模拟 pollClipboard 在 'new' 路径上失败 → 这里没有 new 路径,但语义上
|
||||
// abortLastObserve 应同时把 last / pending 都清掉。下次 tick 读到任何
|
||||
// 内容都重新判 'new',pendign 也不会错误地吞掉外部真实复制。
|
||||
s.abortLastObserve();
|
||||
assert.strictEqual(s.observe('anything'), 'new', 'abort 后 last 必须为空');
|
||||
// 再 markOwnWrite 后能正常工作(说明 pending 也被清掉了,没残留)
|
||||
s.markOwnWrite('P');
|
||||
assert.strictEqual(s.observe('P'), 'unchanged', 'markOwnWrite 在 abort 后照常工作');
|
||||
});
|
||||
|
||||
check('abortLastObserve:无副作用(last/pending 本来就 null 时调用不破坏)', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.abortLastObserve(); // last=null, pending=null,无副作用
|
||||
assert.strictEqual(s.observe('A'), 'new');
|
||||
s.abortLastObserve(); // last='A' → null;下次再判 'new'
|
||||
assert.strictEqual(s.observe('A'), 'new');
|
||||
});
|
||||
|
||||
check('图片场景端到端:自己的图不入库、外部的图入库', () => {
|
||||
const s = createEchoSuppress();
|
||||
s.markOwnWrite('our-hash');
|
||||
assert.strictEqual(s.observe('our-hash'), 'unchanged'); // 与预置基线一致,不入库
|
||||
assert.strictEqual(s.observe('external-hash'), 'new'); // 外部换了图
|
||||
assert.strictEqual(s.observe('external-hash'), 'unchanged');
|
||||
});
|
||||
|
||||
check('文本与图片两份状态互不串扰(main.js 用两个独立实例)', () => {
|
||||
const text = createEchoSuppress();
|
||||
const image = createEchoSuppress();
|
||||
text.markOwnWrite('A');
|
||||
assert.strictEqual(image.observe('A'), 'new', '图片状态机不该认识文本的 pending');
|
||||
assert.strictEqual(text.observe('A'), 'unchanged', '文本状态机命中自己的预置基线');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} checks passed`);
|
||||
Reference in New Issue
Block a user