168 lines
5.6 KiB
JavaScript
168 lines
5.6 KiB
JavaScript
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
|
|
import { addItem } from '../push/queue.js';
|
||
|
|
import { processQueue } from '../push/worker.js';
|
||
|
|
|
||
|
|
const DB_NAME = 'inquiry_push';
|
||
|
|
|
||
|
|
async function dumpAll() {
|
||
|
|
// Bypass getPending (which filters by status/nextRetry) to inspect raw rows.
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const req = indexedDB.open(DB_NAME);
|
||
|
|
req.onsuccess = () => {
|
||
|
|
const db = req.result;
|
||
|
|
const tx = db.transaction('queue', 'readonly').objectStore('queue').getAll();
|
||
|
|
tx.onsuccess = () => { db.close(); resolve(tx.result); };
|
||
|
|
tx.onerror = () => { db.close(); reject(tx.error); };
|
||
|
|
};
|
||
|
|
req.onerror = () => reject(req.error);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// worker 无状态:每次 processQueue 自己从 chrome.storage 读配置(模拟已保存的启用配置)。
|
||
|
|
// 不再用 configure() 注入 —— 模块级配置会在 SW 休眠重启后丢失(正是 B 的根因)。
|
||
|
|
function mockStorage(config) {
|
||
|
|
globalThis.chrome = {
|
||
|
|
storage: { local: { get: vi.fn().mockResolvedValue({ push_config: config }) } },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
const dbs = (await indexedDB.databases?.().catch(() => null)) || [];
|
||
|
|
const names = dbs.length ? dbs.map(d => d.name) : [DB_NAME];
|
||
|
|
await Promise.all(names.map(name => new Promise(r => {
|
||
|
|
if (!name) { r(); return; }
|
||
|
|
const req = indexedDB.deleteDatabase(name);
|
||
|
|
req.onsuccess = req.onerror = req.onblocked = () => r();
|
||
|
|
})));
|
||
|
|
mockStorage({ enabled: true, serverUrl: 'http://test', token: 'tok' });
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('worker', () => {
|
||
|
|
it('pushes when SW resumed without configure() — reads config from storage (regression for B)', async () => {
|
||
|
|
// SW 休眠重启后模块级配置丢失;worker 必须自己从 storage 读,不能依赖 configure()。
|
||
|
|
// beforeEach 已 mock 启用配置,此处故意不调 configure —— 复现并锁定 B 的修复。
|
||
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
||
|
|
await addItem({
|
||
|
|
payload: {
|
||
|
|
record: {
|
||
|
|
inquiryId: 'X',
|
||
|
|
createTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
updateTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
},
|
||
|
|
messages: [],
|
||
|
|
},
|
||
|
|
inquiryId: 'X',
|
||
|
|
});
|
||
|
|
const r = await processQueue();
|
||
|
|
expect(r.processed).toBe(1);
|
||
|
|
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('POSTs pending, marks sent on 200', async () => {
|
||
|
|
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
||
|
|
globalThis.fetch = fetchMock;
|
||
|
|
const id = await addItem({
|
||
|
|
payload: {
|
||
|
|
record: {
|
||
|
|
inquiryId: 'X',
|
||
|
|
createTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
updateTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
},
|
||
|
|
messages: [
|
||
|
|
{
|
||
|
|
messageId: 'm1',
|
||
|
|
sendTime: '2026-01-01T00:00:00Z',
|
||
|
|
direction: 'outgoing',
|
||
|
|
content: 'c',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
inquiryId: 'X',
|
||
|
|
});
|
||
|
|
const r = await processQueue();
|
||
|
|
expect(r.processed).toBe(1);
|
||
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||
|
|
expect(fetchMock.mock.calls[0][0]).toBe('http://test/api/ingest');
|
||
|
|
const all = await dumpAll();
|
||
|
|
expect(all.find(x => x.id === id).status).toBe('sent');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('successful push counts as sent in getStats (not silently removed)', async () => {
|
||
|
|
// 显示 bug 回归:成功推送的记录必须能在 getStats 里看到(sent),
|
||
|
|
// 否则 popup 永远显示"已推 0"(旧实现成功即 remove → 统计不到)。
|
||
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
||
|
|
await addItem({
|
||
|
|
payload: {
|
||
|
|
record: {
|
||
|
|
inquiryId: 'X',
|
||
|
|
createTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
updateTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
},
|
||
|
|
messages: [],
|
||
|
|
},
|
||
|
|
inquiryId: 'X',
|
||
|
|
});
|
||
|
|
await processQueue();
|
||
|
|
const { getStats } = await import('../push/queue.js');
|
||
|
|
const stats = await getStats();
|
||
|
|
expect(stats.sent).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('on failure, retries++ and nextRetry in future', async () => {
|
||
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network'));
|
||
|
|
const id = await addItem({
|
||
|
|
payload: {
|
||
|
|
record: {
|
||
|
|
inquiryId: 'X',
|
||
|
|
createTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
updateTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
},
|
||
|
|
messages: [
|
||
|
|
{
|
||
|
|
messageId: 'm',
|
||
|
|
sendTime: '2026-01-01T00:00:00Z',
|
||
|
|
direction: 'outgoing',
|
||
|
|
content: 'c',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
inquiryId: 'X',
|
||
|
|
});
|
||
|
|
await processQueue();
|
||
|
|
const all = await dumpAll();
|
||
|
|
const item = all.find(x => x.id === id);
|
||
|
|
expect(item.status).toBe('pending');
|
||
|
|
expect(item.retries).toBe(1);
|
||
|
|
expect(item.nextRetry).toBeGreaterThan(Date.now());
|
||
|
|
});
|
||
|
|
|
||
|
|
it('after MAX_RETRIES, mark failed', async () => {
|
||
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network'));
|
||
|
|
const id = await addItem({
|
||
|
|
payload: {
|
||
|
|
record: {
|
||
|
|
inquiryId: 'X',
|
||
|
|
createTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
updateTimeFromApi: '2026-01-01T00:00:00Z',
|
||
|
|
},
|
||
|
|
messages: [
|
||
|
|
{
|
||
|
|
messageId: 'm',
|
||
|
|
sendTime: '2026-01-01T00:00:00Z',
|
||
|
|
direction: 'outgoing',
|
||
|
|
content: 'c',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
inquiryId: 'X',
|
||
|
|
});
|
||
|
|
// Pre-seed retries=7 so one more failure pushes it to 8 -> failed.
|
||
|
|
const { markStatus } = await import('../push/queue.js');
|
||
|
|
await markStatus(id, 'pending', { retries: 7, nextRetry: 0 });
|
||
|
|
await processQueue();
|
||
|
|
const all = await dumpAll();
|
||
|
|
const item = all.find(x => x.id === id);
|
||
|
|
expect(item.status).toBe('failed');
|
||
|
|
});
|
||
|
|
});
|