NapCatQQ source snapshot
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
Release Tag / validate-version (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:01:47 +08:00
commit f954252a34
1111 files changed
+139867

No files matched your search

@@ -0,0 +1,160 @@
// StreamBasic must be evaluated before OneBotAction because of their circular import.
import '../napcat-onebot/action/stream/StreamBasic';
import { describe, expect, it, vi } from 'vitest';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { GetQzoneCommentList } from '@/napcat-onebot/action/extends/qzone/GetQzoneCommentList';
import { GetQzoneCommentReplies } from '@/napcat-onebot/action/extends/qzone/GetQzoneCommentReplies';
import { QzoneFeedReferenceExample } from '@/napcat-onebot/action/extends/qzone/common';
import { QzoneCommentPageExample, QzoneReplyPageExample } from '@/napcat-onebot/action/extends/qzone/common-comment';
function createCore () {
return {
apis: {
QzoneApi: {
getComments: vi.fn().mockResolvedValue(QzoneCommentPageExample),
getCommentReplies: vi.fn().mockResolvedValue(QzoneReplyPageExample),
},
},
context: { logger: { logError: vi.fn() } },
};
}
describe('QQ空间评论读取 actions', () => {
it('passes the complete reference and optional opaque cursor to comment reads', async () => {
const core = createCore();
const action = new GetQzoneCommentList(undefined as never, core as never);
const reference = {
...QzoneFeedReferenceExample,
union_id: { ...QzoneFeedReferenceExample.union_id, unknown_field: { large_id: '9007199254740993' } },
cell_feed_transfer: { opaque: [0, false, 'keep'] },
};
const expected = structuredClone(reference);
const first = await action.handle({ feed_ref: reference }, 'test', {} as never);
await action.handle({ feed_ref: reference, cursor: 'next-comment-cursor' }, 'test', {} as never);
expect(core.apis.QzoneApi.getComments.mock.calls).toEqual([[expected, undefined], [expected, 'next-comment-cursor']]);
expect(first.data).toBe(QzoneCommentPageExample);
expect(reference).toEqual(expected);
});
it('retains raw parent identity types, UID-only authors and every unknown field through handle parsing', async () => {
const core = createCore();
const action = new GetQzoneCommentReplies(undefined as never, core as never);
const parent = {
id: 0,
user: { uid: 'u_commenter', uin: '', nick: '', portrait: { source: 'opaque-portrait' }, unknown_user: 'keep' },
content: [{ type: 0, content: '原评论', unknown_segment: true }],
replys: [{ id: 'reply-1', content: [{ type: 0, content: '内联回复' }] }],
reply_attach_info: 'server-reply-pointer',
like: { id: 'like-key', count: 0, status: 0 },
unknown_field: { large_id: '9007199254740993' },
};
const expected = structuredClone(parent);
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: parent }, 'test', {} as never);
await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: { id: '0' }, cursor: 'next-reply-cursor' }, 'test', {} as never);
expect(result.status).toBe('ok');
expect(core.apis.QzoneApi.getCommentReplies.mock.calls).toEqual([
[QzoneFeedReferenceExample, expected, undefined],
[QzoneFeedReferenceExample, { id: '0' }, 'next-reply-cursor'],
]);
expect(typeof core.apis.QzoneApi.getCommentReplies.mock.calls[0]?.[1]?.id).toBe('number');
expect(typeof core.apis.QzoneApi.getCommentReplies.mock.calls[1]?.[1]?.id).toBe('string');
expect(parent).toEqual(expected);
});
it('allows an omitted author or empty auxiliary user object when the parent ID is exact', async () => {
const core = createCore();
const action = new GetQzoneCommentReplies(undefined as never, core as never);
for (const parent of [{ id: 'parent' }, { id: 'parent', user: {} }, { id: 0, user: { uin: '', uid: '' } }]) {
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: parent }, 'test', {} as never);
expect(result.status).toBe('ok');
}
expect(core.apis.QzoneApi.getCommentReplies).toHaveBeenCalledTimes(3);
});
it('rejects malformed parent IDs before TypeBox can coerce them or invoke the core', async () => {
const core = createCore();
const action = new GetQzoneCommentReplies(undefined as never, core as never);
for (const id of [undefined, null, false, true, '', ' ', -1, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, {}, []]) {
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: { id } } as never, 'test', {} as never);
expect(result.status).toBe('failed');
}
expect(core.apis.QzoneApi.getCommentReplies).not.toHaveBeenCalled();
});
it('rejects malformed read payloads and unsupported page controls before invoking the core', async () => {
const cases = [
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, page_size: 100 }],
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: false }],
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: '' }],
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: 'x'.repeat(16385) }],
[GetQzoneCommentList, { feed_ref: { union_id: {} } }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: [] }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', user: [] } }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', user: null } }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', content: '中'.repeat(22 * 1024) } }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent' }, page_size: 20 }],
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent' }, target_uin: '10002' }],
] as const;
for (const [Action, payload] of cases) {
const core = createCore();
const result = await new Action(undefined as never, core as never).handle(payload as never, 'test', {} as never);
expect(result.status).toBe('failed');
expect(core.apis.QzoneApi.getComments).not.toHaveBeenCalled();
expect(core.apis.QzoneApi.getCommentReplies).not.toHaveBeenCalled();
}
});
it('returns partial and unknown metadata exactly instead of claiming complete comment history', async () => {
const core = createCore();
const page = {
...QzoneCommentPageExample,
comment_num: null,
comments: [{ ...QzoneCommentPageExample.comments[0]!, reply_num: null, has_more_replies: null, replies: [] }],
has_more: true,
next_cursor: 'next-page',
raw: { unknown_server_field: true },
};
core.apis.QzoneApi.getComments.mockResolvedValueOnce(page);
const result = await new GetQzoneCommentList(undefined as never, core as never).handle(
{ feed_ref: QzoneFeedReferenceExample }, 'test', {} as never
);
expect(result.data).toBe(page);
expect(page.comments[0]?.has_more_replies).toBeNull();
expect(page.raw).toEqual({ unknown_server_field: true });
});
it('surfaces cursor failures without retrying the read', async () => {
const core = createCore();
core.apis.QzoneApi.getComments.mockRejectedValueOnce(new Error('QQ空间评论游标无效或已过期'));
const result = await new GetQzoneCommentList(undefined as never, core as never).handle(
{ feed_ref: QzoneFeedReferenceExample, cursor: 'expired-cursor' }, 'test', {} as never
);
expect(result.status).toBe('failed');
expect(result.message).toContain('游标无效或已过期');
expect(core.apis.QzoneApi.getComments).toHaveBeenCalledTimes(1);
});
it('declares complete API metadata with schema-valid detailed examples', () => {
const core = createCore();
const actions = [
new GetQzoneCommentList(undefined as never, core as never),
new GetQzoneCommentReplies(undefined as never, core as never),
];
expect(actions.map(action => action.actionName)).toEqual(['get_qzone_comment_list', 'get_qzone_comment_replies']);
for (const action of actions) {
expect(action.actionSummary).not.toBe('');
expect(action.actionDescription).not.toBe('');
expect(action.actionTags).toContain('扩展接口');
expect(TypeCompiler.Compile(action.payloadSchema).Check(action.payloadExample)).toBe(true);
expect(TypeCompiler.Compile(action.returnSchema).Check(action.returnExample)).toBe(true);
}
});
});