Files
Clipboard/test/echo-suppress.test.js
2026-09-12 13:59:12 +08:00

183 lines
9.4 KiB
JavaScript
Raw Permalink 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.
'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 之间用户在外面复制了 Btick 直接读到 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('resetBaseline1×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`);