Files
peoplelib/src/_test/range-sessions.test.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 3ccd044527 feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0
新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件,
批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、
图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文,
页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。

便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化,
避免升级后 data/ 被遗留在旧目录。

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 12:13:02 +08:00

178 lines
6.3 KiB
JavaScript

const assert = require('node:assert');
const test = require('node:test');
const fs = require('fs');
const os = require('os');
const path = require('path');
const modulePath = require.resolve('../reader/range-sessions');
const dirs = [];
function fixture() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'peoplelib-range-sessions-'));
dirs.push(dir);
const file = path.join(dir, 'fixture.pdf');
const bytes = Buffer.alloc(6 * 1024 * 1024);
for (let index = 0; index < bytes.length; index++) bytes[index] = index % 251;
fs.writeFileSync(file, bytes);
delete require.cache[modulePath];
const sessions = require(modulePath);
sessions.init((_entryId, fileIndex) => ({
abs: file,
format: 'pdf',
fileIndex: Number.isInteger(fileIndex) ? fileIndex : 0
}));
return { sessions, file, bytes };
}
test.after(async () => {
try {
const sessions = require(modulePath);
await sessions.closeAll();
} catch (error) { /* ignore */ }
for (const dir of dirs) {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (error) { /* ignore */ }
}
});
test('PDF 分段会话只返回请求范围并可显式关闭', async () => {
const { sessions, bytes } = fixture();
const opened = await sessions.open(10, 'entry', 0);
assert.match(opened.sessionId, /^[a-f0-9-]{36}$/);
assert.strictEqual(opened.size, bytes.length);
assert.strictEqual(opened.chunkSize, sessions.RANGE_CHUNK_BYTES);
const begin = 1024 * 1024 + 137;
const end = begin + 256 * 1024;
const result = await sessions.read(10, opened.sessionId, begin, end);
assert.deepStrictEqual(result, bytes.subarray(begin, end));
assert.strictEqual(await sessions.close(10, opened.sessionId), true);
assert.strictEqual(await sessions.close(10, opened.sessionId), false);
assert.strictEqual(sessions.status().sessions, 0);
});
test('PDF 分段会话绑定发送者并限制范围与并发会话数量', async () => {
const { sessions, bytes } = fixture();
const first = await sessions.open(20, 'entry', 0);
await assert.rejects(() => sessions.read(21, first.sessionId, 0, 1024), /无效或已关闭/);
await assert.rejects(() => sessions.read(20, first.sessionId, -1, 1024), /范围无效/);
await assert.rejects(
() => sessions.read(20, first.sessionId, 0, sessions.MAX_RANGE_BYTES + 1),
/不能超过 4 MB/
);
await assert.rejects(
() => sessions.read(20, first.sessionId, bytes.length - 10, bytes.length + 1),
/范围无效/
);
const ids = [first.sessionId];
for (let index = 0; index < sessions.MAX_SESSIONS_PER_SENDER; index++) {
ids.push((await sessions.open(20, 'entry', 0)).sessionId);
}
assert.strictEqual(sessions.status().sessions, sessions.MAX_SESSIONS_PER_SENDER);
await assert.rejects(() => sessions.read(20, ids[0], 0, 1024), /无效或已关闭/);
assert.strictEqual(await sessions.closeSender(20), sessions.MAX_SESSIONS_PER_SENDER);
assert.strictEqual(sessions.status().sessions, 0);
});
test('PDF 在阅读期间发生变化时拒绝继续提供旧会话数据', async () => {
const { sessions, file } = fixture();
const opened = await sessions.open(30, 'entry', 0);
fs.appendFileSync(file, Buffer.from([1]));
await assert.rejects(
() => sessions.read(30, opened.sessionId, 0, 1024),
/发生变化/
);
await sessions.closeSender(30);
});
test('40 GB 文件使用安全整数偏移按需读取而不分配整文件缓冲区', async () => {
delete require.cache[modulePath];
const sessions = require(modulePath);
const size = 40 * 1024 * 1024 * 1024;
let closed = false;
const stat = { size, mtimeMs: 1, ctimeMs: 1, isFile: () => true };
const handle = {
stat: async () => stat,
read: async (buffer, offset, length, position) => {
for (let index = 0; index < length; index++) {
buffer[offset + index] = (position + index) % 251;
}
return { bytesRead: length, buffer };
},
close: async () => { closed = true; }
};
sessions.init(
() => ({ abs: 'virtual-40gb.pdf', format: 'pdf', fileIndex: 0 }),
{ promises: { open: async () => handle } }
);
const opened = await sessions.open(40, 'huge', 0);
assert.strictEqual(opened.size, size);
const begin = size - 8192;
const result = await sessions.read(40, opened.sessionId, begin, begin + 4096);
assert.strictEqual(result.length, 4096);
assert.strictEqual(result[0], begin % 251);
assert.strictEqual(result[4095], (begin + 4095) % 251);
await sessions.closeSender(40);
assert.strictEqual(closed, true);
});
test('发送者销毁与会话创建竞态不会遗留文件句柄', async () => {
delete require.cache[modulePath];
const sessions = require(modulePath);
const stat = { size: 4096, mtimeMs: 1, ctimeMs: 1, isFile: () => true };
let releaseOpen;
let closed = false;
sessions.init(
() => ({ abs: 'delayed.pdf', format: 'pdf', fileIndex: 0 }),
{
promises: {
open: () => new Promise((resolve) => {
releaseOpen = () => resolve({
stat: async () => stat,
read: async () => ({ bytesRead: 0 }),
close: async () => { closed = true; }
});
})
}
}
);
const opening = sessions.open(50, 'entry', 0);
while (!releaseOpen) await new Promise((resolve) => setImmediate(resolve));
await sessions.closeSender(50);
releaseOpen();
await assert.rejects(opening, /窗口已关闭/);
assert.strictEqual(closed, true);
assert.strictEqual(sessions.status().sessions, 0);
});
test('范围读取完成后再次校验文件签名', async () => {
delete require.cache[modulePath];
const sessions = require(modulePath);
let changed = false;
let closed = false;
const handle = {
stat: async () => ({
size: 4096,
mtimeMs: changed ? 2 : 1,
ctimeMs: 1,
isFile: () => true
}),
read: async (buffer, offset, length) => {
buffer.fill(1, offset, offset + length);
changed = true;
return { bytesRead: length, buffer };
},
close: async () => { closed = true; }
};
sessions.init(
() => ({ abs: 'changing.pdf', format: 'pdf', fileIndex: 0 }),
{ promises: { open: async () => handle } }
);
const opened = await sessions.open(60, 'entry', 0);
await assert.rejects(
() => sessions.read(60, opened.sessionId, 0, 1024),
/发生变化/
);
while (!closed) await new Promise((resolve) => setImmediate(resolve));
assert.strictEqual(sessions.status().sessions, 0);
});