SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,361 @@
// MessageApi history pagination: bidirectional chunking, dedup, short-cap
// handling, floor termination, and chronological output. Range fetches are
// mocked so this exercises cursor logic rather than wire decoding.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@snowluma/protocol/msg-push', async (importOriginal) => {
const actual = await importOriginal<typeof import('@snowluma/protocol/msg-push')>();
return {
...actual,
fetchGroupMessageRange: vi.fn(),
fetchC2cMessageRange: vi.fn(),
fetchC2cRoamMessagePage: vi.fn(),
};
});
import {
fetchC2cMessageRange,
fetchC2cRoamMessagePage,
fetchGroupMessageRange,
} from '@snowluma/protocol/msg-push';
import { MessageApi } from '../src/bridge/apis/message';
const mockFetch = vi.mocked(fetchGroupMessageRange);
const mockC2cFetch = vi.mocked(fetchC2cMessageRange);
const mockC2cRoamFetch = vi.mocked(fetchC2cRoamMessagePage);
function gm(seq: number): any {
return {
kind: 'group_message',
groupName: '',
time: 0,
selfUin: 0,
senderUin: 1,
msgSeq: seq,
msgId: seq,
elements: [],
groupId: 9999,
senderNick: '',
senderCard: '',
senderRole: '',
};
}
const ctx = { identity: {}, sendRawPacket: vi.fn() } as any;
describe('MessageApi history pagination', () => {
// The walk-back loop enforces a real ~300ms gap between each SsoGetGroupMsg
// send (a deliberate anti-flood throttle, unchanged in production). Fake the
// clock so the test exercises the loop/cursor logic instantly instead of
// waiting out 300ms × up-to-12 real sends.
beforeEach(() => {
mockFetch.mockReset();
mockC2cFetch.mockReset();
mockC2cRoamFetch.mockReset();
vi.useFakeTimers();
});
afterEach(() => vi.useRealTimers());
// Drives a throttled getGroupHistory to completion under fake timers: kick it
// off, flush the queued send-gap timers, then await the result.
async function runHistory<T>(promise: Promise<T>): Promise<T> {
await vi.runAllTimersAsync();
return promise;
}
it('walks back across short-capped windows, dedups, returns newest `want` ascending', async () => {
// Server caps every window to its newest 10 sequences → forces walk-back.
mockFetch.mockImplementation(async (_s, _id, _self, _g, start, end) => {
const out = [];
for (let s = Math.max(start, end - 9); s <= end; s++) {
out.push(gm(s));
}
return out;
});
const res = await runHistory(new MessageApi(ctx).getGroupHistory(9999, 200, 50));
expect(res).toHaveLength(50);
expect(res[0].msgSeq).toBe(151); // newest 50 ending at 200 → 151..200
expect(res[res.length - 1].msgSeq).toBe(200);
for (let i = 1; i < res.length; i++) {
expect(res[i].msgSeq).toBeGreaterThan(res[i - 1].msgSeq); // strictly ascending, no dups
}
});
it('walks forward from and including the anchor when reverse order is disabled', async () => {
mockFetch.mockImplementation(async (_s, _id, _self, _g, start, end) => {
const out = [];
for (let s = start; s <= end; s++) {
out.push(gm(s));
}
return out;
});
const res = await runHistory(new MessageApi(ctx).getGroupHistory(9999, 100, 5, 0, false));
expect(res.map((message) => message.msgSeq)).toEqual([100, 101, 102, 103, 104]);
});
it('does not skip the anchor-side prefix when a forward window is short-capped', async () => {
mockFetch.mockImplementation(async (_s, _id, _self, _g, start, end) => {
const out = [];
for (let s = Math.max(start, end - 9); s <= end; s++) {
out.push(gm(s));
}
return out;
});
const res = await runHistory(new MessageApi(ctx).getGroupHistory(9999, 100, 20, 0, false));
expect(res.map((message) => message.msgSeq)).toEqual(
Array.from({ length: 20 }, (_, index) => 100 + index),
);
});
it('uses the same forward semantics for private history', async () => {
mockC2cFetch.mockImplementation(async (_s, _id, _self, _uid, start, end) => {
const out = [];
for (let s = start; s <= end; s++) {
out.push({ ...gm(s + 10_000), kind: 'friend_message', ntMsgSeq: s });
}
return out;
});
const res = await runHistory(
new MessageApi(ctx).getC2cHistory('u_friend', 300, 3, 0, false),
);
expect(res.map((message) => message.ntMsgSeq)).toEqual([300, 301, 302]);
});
it('fetches the latest private page by timestamp without inventing a sequence anchor', async () => {
mockC2cRoamFetch.mockResolvedValue({
cursor: { time: 0, random: 0 },
messages: [
{ ...gm(32742), kind: 'friend_message', ntMsgSeq: 63213, time: 1700000002 },
{ ...gm(32743), kind: 'friend_message', ntMsgSeq: 63214, time: 1700000001 },
],
} as any);
const result = await runHistory(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 20, 10001, 1700000100),
);
expect(mockC2cRoamFetch).toHaveBeenCalledWith(
ctx,
ctx.identity,
10001,
'u_friend',
1700000100,
20,
0,
);
expect(result.map((message) => message.ntMsgSeq)).toEqual([63213, 63214]);
expect(mockC2cFetch).not.toHaveBeenCalled();
});
it('follows the private roam cursor until the requested count is filled', async () => {
mockC2cRoamFetch
.mockResolvedValueOnce({
cursor: { time: 1700000000, random: 7 },
messages: [
{ ...gm(32744), kind: 'friend_message', ntMsgSeq: 63215, time: 1700000002 },
],
} as any)
.mockResolvedValueOnce({
cursor: { time: 1699999900, random: 8 },
messages: [
{ ...gm(32742), kind: 'friend_message', ntMsgSeq: 63213, time: 1700000000 },
{ ...gm(32743), kind: 'friend_message', ntMsgSeq: 63214, time: 1700000001 },
],
} as any);
const result = await runHistory(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 3, 10001, 1700000100),
);
expect(mockC2cRoamFetch).toHaveBeenNthCalledWith(
1,
ctx,
ctx.identity,
10001,
'u_friend',
1700000100,
3,
0,
);
expect(mockC2cRoamFetch).toHaveBeenNthCalledWith(
2,
ctx,
ctx.identity,
10001,
'u_friend',
1700000000,
2,
7,
);
expect(result.map((message) => message.ntMsgSeq)).toEqual([63213, 63214, 63215]);
});
it('splits a 200-message private history request into bounded pages', async () => {
mockC2cRoamFetch.mockImplementation(
async (_sender, _identity, _self, _friend, time, count) => ({
messages: Array.from({ length: count }, (_, index) => ({
...gm(time - index),
kind: 'friend_message',
ntMsgSeq: time - index,
})),
cursor: { time: time - count, random: 1 },
isComplete: false,
}),
);
const result = await runHistory(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 200, 10001, 1000),
);
expect(result).toHaveLength(200);
expect(mockC2cRoamFetch.mock.calls.map((call) => call[5])).toEqual([100, 100]);
expect(result.map((message) => message.ntMsgSeq)).toEqual(
Array.from({ length: 200 }, (_, index) => 801 + index),
);
});
it('honors completion even when the last page still includes a cursor', async () => {
mockC2cRoamFetch.mockResolvedValue({
messages: [{ ...gm(1), kind: 'friend_message', ntMsgSeq: 1 }],
cursor: { time: 100, random: 2 },
isComplete: true,
});
const result = await runHistory(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 20, 10001, 1000),
);
expect(result).toHaveLength(1);
expect(mockC2cRoamFetch).toHaveBeenCalledOnce();
});
it('does not return partial history after a later request fails', async () => {
mockC2cRoamFetch
.mockResolvedValueOnce({
messages: [{ ...gm(1), kind: 'friend_message', ntMsgSeq: 1 }],
cursor: { time: 100, random: 2 },
isComplete: false,
})
.mockRejectedValueOnce(new Error('SsoGetRoamMsg failed: result=123'));
const assertion = expect(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 20, 10001, 1000),
).rejects.toThrow('result=123');
await vi.runAllTimersAsync();
await assertion;
});
it('rejects a stalled private roam cursor instead of returning a partial page', async () => {
mockC2cRoamFetch.mockResolvedValue({
cursor: { time: 1700000100, random: 0 },
messages: [{ ...gm(32742), kind: 'friend_message', ntMsgSeq: 63213, time: 1700000000 }],
} as any);
const assertion = expect(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 2, 10001, 1700000100),
).rejects.toThrow('cursor did not advance');
await vi.runAllTimersAsync();
await assertion;
});
it('rejects a partial private page when the request budget is exhausted', async () => {
let page = 0;
mockC2cRoamFetch.mockImplementation(async () => {
page++;
return {
cursor: { time: 1700000100 - page, random: page },
messages: [
{
...gm(32000 + page),
kind: 'friend_message',
ntMsgSeq: 63000 + page,
time: 1700000100 - page,
},
],
} as any;
});
const assertion = expect(
new MessageApi(ctx).getC2cLatestHistory('u_friend', 20, 10001, 1700000200),
).rejects.toThrow('request budget exhausted');
await vi.runAllTimersAsync();
await assertion;
expect(mockC2cRoamFetch).toHaveBeenCalledTimes(12);
});
it('stops forward pagination at the unsigned sequence ceiling', async () => {
mockFetch.mockImplementation(async (_s, _id, _self, _g, start, end) => {
const out = [];
for (let s = start; s <= end; s++) {
out.push(gm(s));
}
return out;
});
const res = await runHistory(
new MessageApi(ctx).getGroupHistory(9999, 0xffff_fffe, 5, 0, false),
);
expect(res.map((message) => message.msgSeq)).toEqual([0xffff_fffe, 0xffff_ffff]);
});
it('stops at the sequence floor when fewer messages exist than requested', async () => {
mockFetch.mockImplementation(async (_s, _id, _self, _g, start, end) => {
const out = [];
for (let s = start; s <= end; s++) {
out.push(gm(s));
}
return out;
});
const res = await runHistory(new MessageApi(ctx).getGroupHistory(9999, 20, 100)); // only seq 1..20 exist
expect(res.map((m) => m.msgSeq)).toEqual(Array.from({ length: 20 }, (_, i) => i + 1));
});
it('caps the request count and bounds the request loop', async () => {
let calls = 0;
mockFetch.mockImplementation(async (_s, _id, _self, _g, _start, end) => {
calls++;
return [gm(end)]; // one message per window → never reaches a large `want`
});
const res = await runHistory(new MessageApi(ctx).getGroupHistory(9999, 100_000, 9999));
expect(calls).toBeLessThanOrEqual(12); // HISTORY_MAX_REQUESTS
expect(res.length).toBeLessThanOrEqual(200); // HISTORY_MAX_COUNT
});
it('rejects a failed later window instead of presenting partial history as complete', async () => {
mockFetch
.mockResolvedValueOnce([gm(200)])
.mockRejectedValueOnce(new Error('history transport failed'));
const assertion = expect(
new MessageApi(ctx).getGroupHistory(9999, 200, 20),
).rejects.toThrow('history transport failed');
await vi.runAllTimersAsync();
await assertion;
});
it('returns [] for an invalid anchor without sending', async () => {
expect(await new MessageApi(ctx).getGroupHistory(0, 100, 10)).toEqual([]);
expect(await new MessageApi(ctx).getGroupHistory(9999, 0, 10)).toEqual([]);
expect(mockFetch).not.toHaveBeenCalled();
});
});