@jackwener/opencli 1.7.22 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -148
- package/README.zh-CN.md +37 -211
- package/cli-manifest.json +6423 -4260
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +226 -0
- package/clis/suno/generate.test.js +243 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +62 -0
- package/clis/suno/utils.js +540 -0
- package/clis/suno/utils.test.js +223 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +12 -5
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/search.js +6 -2
- package/clis/twitter/search.test.js +6 -0
- package/clis/twitter/shared.js +144 -0
- package/clis/twitter/shared.test.js +429 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/search-regression.test.js +18 -11
- package/clis/weread/search.js +15 -7
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -1
- package/clis/xiaohongshu/creator-note-detail.test.js +11 -0
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +2 -1
- package/clis/xiaohongshu/creator-notes.test.js +12 -0
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +299 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +12 -0
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +15 -2
- package/clis/zhihu/collection.test.js +46 -0
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -9
- package/clis/zhihu/question.test.js +111 -9
- package/clis/zhihu/search.js +206 -43
- package/clis/zhihu/search.test.js +198 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +110 -0
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import './device-follow.js';
|
|
5
|
+
|
|
6
|
+
const { buildDeviceFollowUrl, extractEntries, joinEntryToTweet, shapeRow, parseDeviceFollow, parseLimit } = await import('./device-follow.js').then((m) => m.__test__);
|
|
7
|
+
|
|
8
|
+
function tweet(id, userId, overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id_str: id,
|
|
11
|
+
user_id_str: userId,
|
|
12
|
+
full_text: `text-${id}`,
|
|
13
|
+
favorite_count: 10,
|
|
14
|
+
retweet_count: 2,
|
|
15
|
+
reply_count: 1,
|
|
16
|
+
quote_count: 0,
|
|
17
|
+
created_at: 'Sun May 18 12:34:56 +0000 2026',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function user(id, screenName) {
|
|
23
|
+
return { id_str: id, screen_name: screenName };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function entry(tweetId) {
|
|
27
|
+
return {
|
|
28
|
+
entryId: `tweet-${tweetId}`,
|
|
29
|
+
content: { item: { content: { tweet: { id: tweetId } } } },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nonTweetEntry(id = 'cursor-bottom') {
|
|
34
|
+
return {
|
|
35
|
+
entryId: id,
|
|
36
|
+
content: { item: { content: {} } },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function payload(tweets, users, entries) {
|
|
41
|
+
return {
|
|
42
|
+
globalObjects: { tweets, users },
|
|
43
|
+
timeline: { id: 'tweet_notifications', instructions: [{ addEntries: { entries } }] },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('twitter device-follow', () => {
|
|
48
|
+
it('parseLimit accepts 1-200 and rejects everything else without silent clamping', () => {
|
|
49
|
+
expect(parseLimit(undefined)).toBe(20);
|
|
50
|
+
expect(parseLimit(1)).toBe(1);
|
|
51
|
+
expect(parseLimit(200)).toBe(200);
|
|
52
|
+
expect(() => parseLimit(0)).toThrow(ArgumentError);
|
|
53
|
+
expect(() => parseLimit(201)).toThrow(ArgumentError);
|
|
54
|
+
expect(() => parseLimit(1.5)).toThrow(ArgumentError);
|
|
55
|
+
expect(() => parseLimit('abc')).toThrow(ArgumentError);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('builds the device_follow URL with required v1.1 params', () => {
|
|
59
|
+
const url = buildDeviceFollowUrl(25);
|
|
60
|
+
expect(url.startsWith('/i/api/2/notifications/device_follow.json?')).toBe(true);
|
|
61
|
+
const params = new URLSearchParams(url.split('?')[1]);
|
|
62
|
+
expect(params.get('count')).toBe('25');
|
|
63
|
+
expect(params.get('tweet_mode')).toBe('extended');
|
|
64
|
+
expect(params.get('include_ext_views')).toBe('true');
|
|
65
|
+
expect(params.get('include_reply_count')).toBe('1');
|
|
66
|
+
expect(params.get('include_quote_count')).toBe('true');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('extracts entries from the addEntries instruction shape', () => {
|
|
70
|
+
const p = payload({}, {}, [entry('1'), entry('2')]);
|
|
71
|
+
const entries = extractEntries(p.timeline);
|
|
72
|
+
expect(entries).toHaveLength(2);
|
|
73
|
+
expect(entries[0].entryId).toBe('tweet-1');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('extractEntries tolerates missing or empty instructions', () => {
|
|
77
|
+
expect(extractEntries(undefined)).toBeNull();
|
|
78
|
+
expect(extractEntries({})).toBeNull();
|
|
79
|
+
expect(extractEntries({ instructions: [] })).toEqual([]);
|
|
80
|
+
expect(extractEntries({ instructions: [{ addEntries: { entries: [] } }] })).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('joinEntryToTweet pairs the entry tweet id to globalObjects', () => {
|
|
84
|
+
const tweets = { '1': tweet('1', 'u1') };
|
|
85
|
+
const users = { u1: user('u1', 'alice') };
|
|
86
|
+
const out = joinEntryToTweet(entry('1'), tweets, users);
|
|
87
|
+
expect(out?.tweetId).toBe('1');
|
|
88
|
+
expect(out?.tweet.full_text).toBe('text-1');
|
|
89
|
+
expect(out?.user.screen_name).toBe('alice');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('joinEntryToTweet returns null when tweet id is missing or unmatched', () => {
|
|
93
|
+
expect(joinEntryToTweet({}, {}, {})).toBeNull();
|
|
94
|
+
expect(joinEntryToTweet(entry('1'), {}, {})).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('shapeRow projects the canonical row schema with views=null', () => {
|
|
98
|
+
const row = shapeRow({
|
|
99
|
+
tweetId: '42',
|
|
100
|
+
tweet: tweet('42', 'u1', { favorite_count: 5, retweet_count: 1, reply_count: 7 }),
|
|
101
|
+
user: user('u1', 'bob'),
|
|
102
|
+
});
|
|
103
|
+
expect(row).toEqual({
|
|
104
|
+
id: '42',
|
|
105
|
+
author: 'bob',
|
|
106
|
+
text: 'text-42',
|
|
107
|
+
likes: 5,
|
|
108
|
+
retweets: 1,
|
|
109
|
+
replies: 7,
|
|
110
|
+
views: null,
|
|
111
|
+
created_at: 'Sun May 18 12:34:56 +0000 2026',
|
|
112
|
+
url: 'https://x.com/bob/status/42',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('joinEntryToTweet requires a resolved user screen_name before emitting a row URL', () => {
|
|
117
|
+
expect(joinEntryToTweet(entry('1'), { '1': tweet('1', 'missing') }, {})).toBeNull();
|
|
118
|
+
expect(joinEntryToTweet(entry('1'), { '1': tweet('1', 'u1') }, { u1: { id_str: 'u1' } })).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('shapeRow uses tweet.text when full_text is absent', () => {
|
|
122
|
+
const row = shapeRow({ tweetId: '7', tweet: { text: 'fallback' }, user: user('u1', 'x') });
|
|
123
|
+
expect(row.text).toBe('fallback');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('parseDeviceFollow maps the legacy v1.1 payload to rows', () => {
|
|
127
|
+
const p = payload(
|
|
128
|
+
{ '1': tweet('1', 'u1'), '2': tweet('2', 'u2') },
|
|
129
|
+
{ u1: user('u1', 'alice'), u2: user('u2', 'bob') },
|
|
130
|
+
[entry('1'), entry('2')],
|
|
131
|
+
);
|
|
132
|
+
const rows = parseDeviceFollow(p, new Set());
|
|
133
|
+
expect(rows?.rows.map((r) => r.author)).toEqual(['alice', 'bob']);
|
|
134
|
+
expect(rows?.rows.every((r) => r.views === null)).toBe(true);
|
|
135
|
+
expect(rows?.rows[0].url).toBe('https://x.com/alice/status/1');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parseDeviceFollow dedupes via the seen set', () => {
|
|
139
|
+
const p = payload(
|
|
140
|
+
{ '1': tweet('1', 'u1') },
|
|
141
|
+
{ u1: user('u1', 'alice') },
|
|
142
|
+
[entry('1'), entry('1')],
|
|
143
|
+
);
|
|
144
|
+
expect(parseDeviceFollow(p, new Set())?.rows).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('parseDeviceFollow returns typed empty metadata for the empty-stream shape', () => {
|
|
148
|
+
const empty = { globalObjects: {}, timeline: { instructions: [{ addEntries: { entries: [] } }] } };
|
|
149
|
+
expect(parseDeviceFollow(empty, new Set())).toEqual({
|
|
150
|
+
rows: [],
|
|
151
|
+
entryCount: 0,
|
|
152
|
+
unmatchedTweetEntries: 0,
|
|
153
|
+
malformedEntries: 0,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('parseDeviceFollow returns null for malformed top-level shape', () => {
|
|
158
|
+
expect(parseDeviceFollow({}, new Set())).toBeNull();
|
|
159
|
+
expect(parseDeviceFollow({ globalObjects: {}, timeline: {} }, new Set())).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('parseDeviceFollow tracks tweet entries that cannot join to required user identity', () => {
|
|
163
|
+
const parsed = parseDeviceFollow(payload({ '1': tweet('1', 'u1') }, {}, [entry('1')]), new Set());
|
|
164
|
+
expect(parsed).toMatchObject({
|
|
165
|
+
rows: [],
|
|
166
|
+
entryCount: 1,
|
|
167
|
+
unmatchedTweetEntries: 1,
|
|
168
|
+
malformedEntries: 0,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('parseDeviceFollow tracks non-empty entries without tweet identity as parser drift', () => {
|
|
173
|
+
const parsed = parseDeviceFollow(payload({}, {}, [nonTweetEntry()]), new Set());
|
|
174
|
+
expect(parsed).toMatchObject({
|
|
175
|
+
rows: [],
|
|
176
|
+
entryCount: 1,
|
|
177
|
+
unmatchedTweetEntries: 0,
|
|
178
|
+
malformedEntries: 1,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('registers with the canonical twitter row columns (minus has_media/media_urls/card)', () => {
|
|
183
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
184
|
+
expect(cmd?.columns).toEqual(['id', 'author', 'text', 'likes', 'retweets', 'replies', 'views', 'created_at', 'url']);
|
|
185
|
+
expect(cmd?.browser).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('throws AuthRequiredError when ct0 cookie is missing', async () => {
|
|
189
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
190
|
+
const page = {
|
|
191
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'other', value: 'x' }]),
|
|
192
|
+
evaluate: vi.fn(),
|
|
193
|
+
};
|
|
194
|
+
await expect(cmd.func(page, { limit: 5 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('throws CommandExecutionError on non-2xx response', async () => {
|
|
198
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
199
|
+
const page = {
|
|
200
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
201
|
+
evaluate: vi.fn().mockResolvedValue({ error: 401 }),
|
|
202
|
+
};
|
|
203
|
+
await expect(cmd.func(page, { limit: 5 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('throws CommandExecutionError on non-auth non-2xx response', async () => {
|
|
207
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
208
|
+
const page = {
|
|
209
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
210
|
+
evaluate: vi.fn().mockResolvedValue({ error: 500 }),
|
|
211
|
+
};
|
|
212
|
+
await expect(cmd.func(page, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('throws CommandExecutionError on browser fetch/json exceptions', async () => {
|
|
216
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
217
|
+
const cookies = [{ name: 'ct0', value: 'token' }];
|
|
218
|
+
await expect(cmd.func({
|
|
219
|
+
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
220
|
+
evaluate: vi.fn().mockResolvedValue({ errorKind: 'non_json', detail: 'Unexpected token <' }),
|
|
221
|
+
}, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
222
|
+
await expect(cmd.func({
|
|
223
|
+
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
224
|
+
evaluate: vi.fn().mockResolvedValue({ errorKind: 'exception', detail: 'network failed' }),
|
|
225
|
+
}, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('throws EmptyResultError for a valid empty device-follow stream', async () => {
|
|
229
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
230
|
+
const page = {
|
|
231
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
232
|
+
evaluate: vi.fn().mockResolvedValue(payload({}, {}, [])),
|
|
233
|
+
};
|
|
234
|
+
await expect(cmd.func(page, { limit: 5 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('throws CommandExecutionError for malformed or unjoinable device-follow payloads', async () => {
|
|
238
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
239
|
+
const cookies = [{ name: 'ct0', value: 'token' }];
|
|
240
|
+
await expect(cmd.func({
|
|
241
|
+
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
242
|
+
evaluate: vi.fn().mockResolvedValue({}),
|
|
243
|
+
}, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
244
|
+
await expect(cmd.func({
|
|
245
|
+
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
246
|
+
evaluate: vi.fn().mockResolvedValue(payload({ '1': tweet('1', 'u1') }, {}, [entry('1')])),
|
|
247
|
+
}, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
248
|
+
await expect(cmd.func({
|
|
249
|
+
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
250
|
+
evaluate: vi.fn().mockResolvedValue(payload({}, {}, [nonTweetEntry()])),
|
|
251
|
+
}, { limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns parsed rows when the fetch succeeds', async () => {
|
|
255
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
256
|
+
const p = payload(
|
|
257
|
+
{ '1': tweet('1', 'u1'), '2': tweet('2', 'u2') },
|
|
258
|
+
{ u1: user('u1', 'alice'), u2: user('u2', 'bob') },
|
|
259
|
+
[entry('1'), entry('2')],
|
|
260
|
+
);
|
|
261
|
+
const page = {
|
|
262
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
263
|
+
evaluate: vi.fn().mockResolvedValue(p),
|
|
264
|
+
};
|
|
265
|
+
const rows = await cmd.func(page, { limit: 5 });
|
|
266
|
+
expect(rows).toHaveLength(2);
|
|
267
|
+
expect(rows[0].author).toBe('alice');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('respects --limit when the upstream returns more than asked', async () => {
|
|
271
|
+
const cmd = getRegistry().get('twitter/device-follow');
|
|
272
|
+
const tweets = {};
|
|
273
|
+
const users = {};
|
|
274
|
+
const entries = [];
|
|
275
|
+
for (let i = 1; i <= 10; i++) {
|
|
276
|
+
tweets[String(i)] = tweet(String(i), `u${i}`);
|
|
277
|
+
users[`u${i}`] = user(`u${i}`, `user${i}`);
|
|
278
|
+
entries.push(entry(String(i)));
|
|
279
|
+
}
|
|
280
|
+
const page = {
|
|
281
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]),
|
|
282
|
+
evaluate: vi.fn().mockResolvedValue(payload(tweets, users, entries)),
|
|
283
|
+
};
|
|
284
|
+
const rows = await cmd.func(page, { limit: 3 });
|
|
285
|
+
expect(rows).toHaveLength(3);
|
|
286
|
+
});
|
|
287
|
+
});
|