@jackwener/opencli 1.7.22 → 1.8.1

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.
Files changed (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -7,6 +7,25 @@
7
7
  */
8
8
  import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
9
9
  const PIXIV_DOMAIN = 'www.pixiv.net';
10
+
11
+ function unwrapEvaluateResult(payload) {
12
+ if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
13
+ return payload.data;
14
+ }
15
+ return payload;
16
+ }
17
+
18
+ function extractPixivErrorMessage(payload) {
19
+ if (!payload || typeof payload !== 'object') return '';
20
+ const candidates = [
21
+ payload.message,
22
+ payload.errorMessage,
23
+ payload.error?.message,
24
+ payload.error,
25
+ ];
26
+ const found = candidates.find((value) => typeof value === 'string' && value.trim());
27
+ return found ? found.trim() : '';
28
+ }
10
29
  /**
11
30
  * Navigate to Pixiv (to attach cookies) then fetch a Pixiv Ajax API endpoint.
12
31
  *
@@ -21,27 +40,57 @@ const PIXIV_DOMAIN = 'www.pixiv.net';
21
40
  * @throws CommandExecutionError on 404 or other HTTP errors
22
41
  */
23
42
  export async function pixivFetch(page, path, opts = {}) {
24
- await page.goto(`https://${PIXIV_DOMAIN}`);
43
+ try {
44
+ await page.goto(`https://${PIXIV_DOMAIN}`);
45
+ } catch (error) {
46
+ throw new CommandExecutionError(`Pixiv navigation failed: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
25
48
  const qs = opts.params
26
49
  ? '?' + Object.entries(opts.params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
27
50
  : '';
28
51
  const url = `https://${PIXIV_DOMAIN}${path}${qs}`;
29
- const data = await page.evaluate(`
52
+ let data;
53
+ try {
54
+ data = unwrapEvaluateResult(await page.evaluate(`
30
55
  (async () => {
31
56
  const res = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
32
- if (!res.ok) return { __httpError: res.status };
33
- return await res.json();
57
+ const text = await res.text();
58
+ let json = null;
59
+ if (text) {
60
+ try { json = JSON.parse(text); } catch {}
61
+ }
62
+ if (!res.ok) {
63
+ return {
64
+ __httpError: res.status,
65
+ message: json?.message || json?.errorMessage || json?.error?.message || (typeof json?.error === 'string' ? json.error : '') || text.slice(0, 200),
66
+ };
67
+ }
68
+ if (!json) return { __malformed: true, message: 'invalid JSON' };
69
+ return json;
34
70
  })()
35
- `);
71
+ `));
72
+ } catch (error) {
73
+ throw new CommandExecutionError(`Pixiv request failed: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
36
75
  if (data?.__httpError) {
37
76
  const status = data.__httpError;
38
77
  if (status === 401 || status === 403) {
39
78
  throw new AuthRequiredError(PIXIV_DOMAIN, 'Authentication required — please log in to Pixiv in Chrome');
40
79
  }
80
+ const message = extractPixivErrorMessage(data);
41
81
  if (status === 404) {
42
- throw new CommandExecutionError(opts.notFoundMsg || `Pixiv resource not found (HTTP 404)`);
82
+ throw new CommandExecutionError(message || opts.notFoundMsg || `Pixiv resource not found (HTTP 404)`);
43
83
  }
44
- throw new CommandExecutionError(`Pixiv request failed (HTTP ${status})`);
84
+ throw new CommandExecutionError(message ? `Pixiv request failed (HTTP ${status}): ${message}` : `Pixiv request failed (HTTP ${status})`);
85
+ }
86
+ if (!data || Array.isArray(data) || typeof data !== 'object' || data.__malformed) {
87
+ throw new CommandExecutionError('Pixiv request returned malformed JSON payload');
88
+ }
89
+ if (data.error === true) {
90
+ throw new CommandExecutionError(extractPixivErrorMessage(data) || 'Pixiv API returned an error');
91
+ }
92
+ if (!('body' in data)) {
93
+ throw new CommandExecutionError('Pixiv request returned malformed API payload');
45
94
  }
46
95
  return data?.body;
47
96
  }
@@ -2,7 +2,7 @@
2
2
  * PowerChina search — browser DOM extraction with multi-entry URL probing.
3
3
  */
4
4
  import { cli, Strategy } from '@jackwener/opencli/registry';
5
- import { AuthRequiredError } from '@jackwener/opencli/errors';
5
+ import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
6
6
  import {
7
7
  cleanText,
8
8
  normalizeDate,
@@ -215,7 +215,7 @@ cli({
215
215
  );
216
216
 
217
217
  if (rows.length === 0 && extractedRows.length > 0) {
218
- throw new Error('[taxonomy=empty_result] site=powerchina command=search extracted only navigation/portal rows');
218
+ throw new EmptyResultError('powerchina search', 'extracted only navigation/portal rows, no bid entries matched');
219
219
  }
220
220
 
221
221
  if (rows.length === 0) {
@@ -227,7 +227,7 @@ cli({
227
227
  );
228
228
  }
229
229
  if (apiFailure) {
230
- throw new Error(`[taxonomy=empty_result] site=powerchina command=search api/dom yielded no result: ${apiFailure}`);
230
+ throw new EmptyResultError('powerchina search', `api/dom yielded no result: ${apiFailure}`);
231
231
  }
232
232
  }
233
233
 
@@ -1,5 +1,12 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
2
4
  import { __test__ } from './search.js';
5
+ import './search.js';
6
+
7
+ afterEach(() => {
8
+ vi.unstubAllGlobals();
9
+ });
3
10
 
4
11
  describe('powerchina search helpers', () => {
5
12
  it('builds candidate URLs with keyword variants', () => {
@@ -64,4 +71,23 @@ describe('powerchina search helpers', () => {
64
71
  expect(mapped?.date).toBe('2026-04-07');
65
72
  expect(mapped?.url).toBe('https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo/2409419657');
66
73
  });
74
+
75
+ it('throws EmptyResultError when extraction only finds navigation rows', async () => {
76
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')));
77
+ const cmd = getRegistry().get('powerchina/search');
78
+ const page = {
79
+ goto: vi.fn().mockResolvedValue(undefined),
80
+ wait: vi.fn().mockResolvedValue(undefined),
81
+ evaluate: vi.fn().mockResolvedValue([
82
+ {
83
+ title: '首页',
84
+ url: 'https://bid.powerchina.cn/',
85
+ date: '',
86
+ contextText: '首页',
87
+ },
88
+ ]),
89
+ };
90
+
91
+ await expect(cmd.func(page, { query: '电梯', limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
92
+ });
67
93
  });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Behavioral tests for the extractRedditMedia helper.
3
+ *
4
+ * The helper is duplicated inline inside each adapter's browser-side evaluate()
5
+ * source (see popular.js / hot.js / search.js / frontpage.js / subreddit.js).
6
+ * Per-adapter tests grep that the same function-name + spread call is present.
7
+ * This file pins the helper's behavior against representative Reddit-JSON
8
+ * fixtures.
9
+ */
10
+ import { describe, expect, it } from 'vitest';
11
+
12
+ function decodeHtml(s) {
13
+ if (typeof s !== 'string' || !s) return '';
14
+ return s
15
+ .replace(/&/g, '&')
16
+ .replace(/&lt;/g, '<')
17
+ .replace(/&gt;/g, '>')
18
+ .replace(/&quot;/g, '"')
19
+ .replace(/&#x27;/gi, "'")
20
+ .replace(/&#39;/g, "'");
21
+ }
22
+ function extractRedditMedia(d) {
23
+ const post_hint = d?.post_hint || '';
24
+ const url_overridden_by_dest = d?.url_overridden_by_dest || '';
25
+ const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
26
+ const gallery_urls = [];
27
+ const items = d?.gallery_data?.items;
28
+ const meta = d?.media_metadata;
29
+ if (Array.isArray(items) && meta) {
30
+ for (const it of items) {
31
+ const m = it && meta[it.media_id];
32
+ const u = m?.s?.u;
33
+ if (u) gallery_urls.push(decodeHtml(u));
34
+ }
35
+ }
36
+ return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
37
+ }
38
+
39
+ describe('extractRedditMedia', () => {
40
+ it('returns empty fields for a plain text/self post', () => {
41
+ expect(extractRedditMedia({ is_self: true, selftext: 'hi' })).toEqual({
42
+ post_hint: '',
43
+ url_overridden_by_dest: '',
44
+ preview_image_url: '',
45
+ gallery_urls: [],
46
+ });
47
+ });
48
+
49
+ it('extracts post_hint, url_overridden_by_dest, preview_image_url for a single-image post', () => {
50
+ const post = {
51
+ post_hint: 'image',
52
+ url_overridden_by_dest: 'https://i.redd.it/abc.jpg',
53
+ preview: {
54
+ images: [{ source: { url: 'https://preview.redd.it/abc.jpg?width=640&amp;s=xyz' } }],
55
+ },
56
+ };
57
+ expect(extractRedditMedia(post)).toEqual({
58
+ post_hint: 'image',
59
+ url_overridden_by_dest: 'https://i.redd.it/abc.jpg',
60
+ preview_image_url: 'https://preview.redd.it/abc.jpg?width=640&s=xyz',
61
+ gallery_urls: [],
62
+ });
63
+ });
64
+
65
+ it('extracts gallery_urls in gallery_data.items order, HTML-decoded', () => {
66
+ const post = {
67
+ post_hint: '',
68
+ url_overridden_by_dest: 'https://www.reddit.com/gallery/xyz',
69
+ gallery_data: {
70
+ items: [{ media_id: 'idB' }, { media_id: 'idA' }, { media_id: 'idC' }],
71
+ },
72
+ media_metadata: {
73
+ idA: { s: { u: 'https://preview.redd.it/idA.jpg?width=1&amp;a=1' } },
74
+ idB: { s: { u: 'https://preview.redd.it/idB.jpg?width=1&amp;a=2' } },
75
+ idC: { s: { u: 'https://preview.redd.it/idC.jpg?width=1&amp;a=3' } },
76
+ },
77
+ };
78
+ const out = extractRedditMedia(post);
79
+ expect(out.gallery_urls).toEqual([
80
+ 'https://preview.redd.it/idB.jpg?width=1&a=2',
81
+ 'https://preview.redd.it/idA.jpg?width=1&a=1',
82
+ 'https://preview.redd.it/idC.jpg?width=1&a=3',
83
+ ]);
84
+ expect(out.url_overridden_by_dest).toBe('https://www.reddit.com/gallery/xyz');
85
+ });
86
+
87
+ it('extracts post_hint for a hosted:video post', () => {
88
+ const post = {
89
+ post_hint: 'hosted:video',
90
+ url_overridden_by_dest: 'https://v.redd.it/xyz',
91
+ preview: { images: [{ source: { url: 'https://preview.redd.it/thumb.jpg' } }] },
92
+ };
93
+ const out = extractRedditMedia(post);
94
+ expect(out.post_hint).toBe('hosted:video');
95
+ expect(out.url_overridden_by_dest).toBe('https://v.redd.it/xyz');
96
+ expect(out.preview_image_url).toBe('https://preview.redd.it/thumb.jpg');
97
+ expect(out.gallery_urls).toEqual([]);
98
+ });
99
+
100
+ it('extracts post_hint for an external link post', () => {
101
+ const post = {
102
+ post_hint: 'link',
103
+ url_overridden_by_dest: 'https://example.com/article',
104
+ };
105
+ expect(extractRedditMedia(post)).toEqual({
106
+ post_hint: 'link',
107
+ url_overridden_by_dest: 'https://example.com/article',
108
+ preview_image_url: '',
109
+ gallery_urls: [],
110
+ });
111
+ });
112
+
113
+ it('HTML-decodes preview URLs that arrive with &amp; separators', () => {
114
+ const post = {
115
+ preview: {
116
+ images: [{ source: { url: 'https://x/?a=1&amp;b=2&amp;c=3' } }],
117
+ },
118
+ };
119
+ expect(extractRedditMedia(post).preview_image_url).toBe('https://x/?a=1&b=2&c=3');
120
+ });
121
+
122
+ it('skips gallery entries whose media_metadata is missing', () => {
123
+ const post = {
124
+ gallery_data: { items: [{ media_id: 'present' }, { media_id: 'orphan' }] },
125
+ media_metadata: {
126
+ present: { s: { u: 'https://preview.redd.it/present.jpg' } },
127
+ // 'orphan' intentionally absent
128
+ },
129
+ };
130
+ expect(extractRedditMedia(post).gallery_urls).toEqual([
131
+ 'https://preview.redd.it/present.jpg',
132
+ ]);
133
+ });
134
+
135
+ it('tolerates null/undefined input without throwing', () => {
136
+ expect(extractRedditMedia(null)).toEqual({
137
+ post_hint: '',
138
+ url_overridden_by_dest: '',
139
+ preview_image_url: '',
140
+ gallery_urls: [],
141
+ });
142
+ expect(extractRedditMedia(undefined)).toEqual({
143
+ post_hint: '',
144
+ url_overridden_by_dest: '',
145
+ preview_image_url: '',
146
+ gallery_urls: [],
147
+ });
148
+ });
149
+ });
@@ -10,22 +10,60 @@ cli({
10
10
  args: [
11
11
  { name: 'limit', type: 'int', default: 15 },
12
12
  ],
13
- columns: ['title', 'subreddit', 'author', 'upvotes', 'comments', 'url'],
13
+ columns: ['title', 'subreddit', 'author', 'upvotes', 'comments', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
14
14
  pipeline: [
15
15
  { navigate: 'https://www.reddit.com' },
16
16
  { evaluate: `(async () => {
17
- const res = await fetch('/r/all.json?limit=\${{ args.limit }}', { credentials: 'include' });
17
+ function decodeHtml(s) {
18
+ if (typeof s !== 'string' || !s) return '';
19
+ return s
20
+ .replace(/&amp;/g, '&')
21
+ .replace(/&lt;/g, '<')
22
+ .replace(/&gt;/g, '>')
23
+ .replace(/&quot;/g, '"')
24
+ .replace(/&#x27;/gi, "'")
25
+ .replace(/&#39;/g, "'");
26
+ }
27
+ function extractRedditMedia(d) {
28
+ const post_hint = d?.post_hint || '';
29
+ const url_overridden_by_dest = d?.url_overridden_by_dest || '';
30
+ const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
31
+ const gallery_urls = [];
32
+ const items = d?.gallery_data?.items;
33
+ const meta = d?.media_metadata;
34
+ if (Array.isArray(items) && meta) {
35
+ for (const it of items) {
36
+ const m = it && meta[it.media_id];
37
+ const u = m?.s?.u;
38
+ if (u) gallery_urls.push(decodeHtml(u));
39
+ }
40
+ }
41
+ return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
42
+ }
43
+ const res = await fetch('/r/all.json?limit=\${{ args.limit }}&raw_json=1', { credentials: 'include' });
18
44
  const j = await res.json();
19
- return j?.data?.children || [];
45
+ return (j?.data?.children || []).map(c => ({
46
+ title: c.data.title,
47
+ subreddit: c.data.subreddit_name_prefixed,
48
+ author: c.data.author,
49
+ upvotes: c.data.score,
50
+ comments: c.data.num_comments,
51
+ url: 'https://www.reddit.com' + c.data.permalink,
52
+ ...extractRedditMedia(c.data),
53
+ }));
20
54
  })()
21
55
  ` },
22
56
  { map: {
23
- title: '${{ item.data.title }}',
24
- subreddit: '${{ item.data.subreddit_name_prefixed }}',
25
- author: '${{ item.data.author }}',
26
- upvotes: '${{ item.data.score }}',
27
- comments: '${{ item.data.num_comments }}',
28
- url: 'https://www.reddit.com${{ item.data.permalink }}',
57
+ title: '${{ item.title }}',
58
+ subreddit: '${{ item.subreddit }}',
59
+ author: '${{ item.author }}',
60
+ upvotes: '${{ item.upvotes }}',
61
+ comments: '${{ item.comments }}',
62
+ url: '${{ item.url }}',
63
+ post_hint: '${{ item.post_hint }}',
64
+ url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
65
+ preview_image_url: '${{ item.preview_image_url }}',
66
+ gallery_urls: '${{ item.gallery_urls }}',
29
67
  } },
30
68
  { limit: '${{ args.limit }}' },
31
69
  ],
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import './frontpage.js';
4
+
5
+ describe('reddit frontpage adapter', () => {
6
+ const command = getRegistry().get('reddit/frontpage');
7
+
8
+ it('exposes the full frontpage shape including the 4 media columns', () => {
9
+ expect(command?.columns).toEqual([
10
+ 'title', 'subreddit', 'author', 'upvotes', 'comments', 'url',
11
+ 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
12
+ ]);
13
+ });
14
+
15
+ it('shapes children into the intermediate-object pattern with media spread in', () => {
16
+ // Refactored from item.data.* templating to a uniform pre-shaped object
17
+ // so gallery_urls (array-valued) can be set directly in evaluate.
18
+ expect(command?.pipeline?.[1]?.evaluate).toContain('function extractRedditMedia');
19
+ expect(command?.pipeline?.[1]?.evaluate).toContain('...extractRedditMedia(c.data)');
20
+ expect(command?.pipeline?.[1]?.evaluate).toContain('/r/all.json?limit=${{ args.limit }}&raw_json=1');
21
+ expect(command?.pipeline?.[2]?.map).toMatchObject({
22
+ title: '${{ item.title }}',
23
+ subreddit: '${{ item.subreddit }}',
24
+ author: '${{ item.author }}',
25
+ upvotes: '${{ item.upvotes }}',
26
+ comments: '${{ item.comments }}',
27
+ url: '${{ item.url }}',
28
+ post_hint: '${{ item.post_hint }}',
29
+ url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
30
+ preview_image_url: '${{ item.preview_image_url }}',
31
+ gallery_urls: '${{ item.gallery_urls }}',
32
+ });
33
+ });
34
+ });
@@ -15,6 +15,35 @@ export function parseRedditHomeLimit(raw) {
15
15
  return n;
16
16
  }
17
17
 
18
+ export function decodeRedditHtml(value) {
19
+ if (typeof value !== 'string' || !value) return '';
20
+ return value
21
+ .replace(/&amp;/g, '&')
22
+ .replace(/&lt;/g, '<')
23
+ .replace(/&gt;/g, '>')
24
+ .replace(/&quot;/g, '"')
25
+ .replace(/&#x27;/gi, "'")
26
+ .replace(/&#39;/g, "'");
27
+ }
28
+
29
+ export function extractRedditMedia(d) {
30
+ const galleryUrls = [];
31
+ const items = d?.gallery_data?.items;
32
+ const meta = d?.media_metadata;
33
+ if (Array.isArray(items) && meta && typeof meta === 'object') {
34
+ for (const item of items) {
35
+ const url = meta[item?.media_id]?.s?.u;
36
+ if (typeof url === 'string' && url) galleryUrls.push(decodeRedditHtml(url));
37
+ }
38
+ }
39
+ return {
40
+ post_hint: typeof d?.post_hint === 'string' ? d.post_hint : '',
41
+ url_overridden_by_dest: decodeRedditHtml(d?.url_overridden_by_dest || ''),
42
+ preview_image_url: decodeRedditHtml(d?.preview?.images?.[0]?.source?.url || ''),
43
+ gallery_urls: galleryUrls,
44
+ };
45
+ }
46
+
18
47
  cli({
19
48
  site: 'reddit',
20
49
  name: 'home',
@@ -26,7 +55,7 @@ cli({
26
55
  args: [
27
56
  { name: 'limit', type: 'int', default: 25, help: `Number of posts (1–${REDDIT_HOME_MAX_LIMIT})` },
28
57
  ],
29
- columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url'],
58
+ columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
30
59
  func: async (page, kwargs) => {
31
60
  const limit = parseRedditHomeLimit(kwargs.limit);
32
61
  await page.goto('https://www.reddit.com');
@@ -103,6 +132,7 @@ cli({
103
132
  postId: d.id,
104
133
  author: typeof d.author === 'string' ? d.author : null,
105
134
  url: d.permalink ? 'https://www.reddit.com' + d.permalink : null,
135
+ ...extractRedditMedia(d),
106
136
  });
107
137
  }
108
138
  if (rows.length === 0) {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { getRegistry } from '@jackwener/opencli/registry';
3
3
  import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
- import { parseRedditHomeLimit } from './home.js';
4
+ import { extractRedditMedia, parseRedditHomeLimit } from './home.js';
5
5
  import './home.js';
6
6
 
7
7
  function makePage(result) {
@@ -33,7 +33,10 @@ describe('reddit home command', () => {
33
33
  expect(command).toBeDefined();
34
34
  expect(command.access).toBe('read');
35
35
  expect(command.browser).toBe(true);
36
- expect(command.columns).toEqual(['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url']);
36
+ expect(command.columns).toEqual([
37
+ 'rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url',
38
+ 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
39
+ ]);
37
40
  });
38
41
 
39
42
  it('parseRedditHomeLimit accepts [1,100] and rejects out-of-range / non-integer without silent clamp', () => {
@@ -81,20 +84,60 @@ describe('reddit home command', () => {
81
84
  {
82
85
  rank: 1, title: 'Title for a1', subreddit: 'r/dummy', score: 100, comments: 10,
83
86
  postId: 'a1', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/a1/title/',
87
+ post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
84
88
  },
85
89
  {
86
90
  rank: 2, title: 'Title for b2', subreddit: 'r/dummy', score: 250, comments: 42,
87
91
  postId: 'b2', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/b2/title/',
92
+ post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
88
93
  },
89
94
  ]);
90
95
  // Row shape must match declared columns exactly.
91
96
  for (const row of rows) {
92
97
  expect(Object.keys(row).sort()).toEqual(
93
- ['author', 'comments', 'postId', 'rank', 'score', 'subreddit', 'title', 'url'],
98
+ [
99
+ 'author', 'comments', 'gallery_urls', 'postId', 'post_hint', 'preview_image_url',
100
+ 'rank', 'score', 'subreddit', 'title', 'url', 'url_overridden_by_dest',
101
+ ],
94
102
  );
95
103
  }
96
104
  });
97
105
 
106
+ it('surfaces media route fields from the personalized home feed', async () => {
107
+ const entries = [makeEntry('a1', {
108
+ post_hint: 'image',
109
+ url_overridden_by_dest: 'https://i.redd.it/a.jpg',
110
+ preview: {
111
+ images: [{ source: { url: 'https://preview.redd.it/a.jpg?x=1&amp;y=2' } }],
112
+ },
113
+ gallery_data: { items: [{ media_id: 'm2' }, { media_id: 'm1' }] },
114
+ media_metadata: {
115
+ m1: { s: { u: 'https://preview.redd.it/m1.jpg?x=1&amp;y=1' } },
116
+ m2: { s: { u: 'https://preview.redd.it/m2.jpg?x=1&amp;y=2' } },
117
+ },
118
+ })];
119
+ const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 25 });
120
+
121
+ expect(rows[0]).toMatchObject({
122
+ post_hint: 'image',
123
+ url_overridden_by_dest: 'https://i.redd.it/a.jpg',
124
+ preview_image_url: 'https://preview.redd.it/a.jpg?x=1&y=2',
125
+ gallery_urls: [
126
+ 'https://preview.redd.it/m2.jpg?x=1&y=2',
127
+ 'https://preview.redd.it/m1.jpg?x=1&y=1',
128
+ ],
129
+ });
130
+ });
131
+
132
+ it('extractRedditMedia tolerates missing media without throwing', () => {
133
+ expect(extractRedditMedia({ is_self: true })).toEqual({
134
+ post_hint: '',
135
+ url_overridden_by_dest: '',
136
+ preview_image_url: '',
137
+ gallery_urls: [],
138
+ });
139
+ });
140
+
98
141
  it('applies the post-fetch limit slice (defence in depth vs Reddit overshoot)', async () => {
99
142
  const entries = Array.from({ length: 30 }, (_, i) => makeEntry(`p${i}`));
100
143
  const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 5 });
@@ -13,10 +13,36 @@ cli({
13
13
  },
14
14
  { name: 'limit', type: 'int', default: 20, help: 'Number of posts' },
15
15
  ],
16
- columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url'],
16
+ columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
17
17
  pipeline: [
18
18
  { navigate: 'https://www.reddit.com' },
19
19
  { evaluate: `(async () => {
20
+ function decodeHtml(s) {
21
+ if (typeof s !== 'string' || !s) return '';
22
+ return s
23
+ .replace(/&amp;/g, '&')
24
+ .replace(/&lt;/g, '<')
25
+ .replace(/&gt;/g, '>')
26
+ .replace(/&quot;/g, '"')
27
+ .replace(/&#x27;/gi, "'")
28
+ .replace(/&#39;/g, "'");
29
+ }
30
+ function extractRedditMedia(d) {
31
+ const post_hint = d?.post_hint || '';
32
+ const url_overridden_by_dest = d?.url_overridden_by_dest || '';
33
+ const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
34
+ const gallery_urls = [];
35
+ const items = d?.gallery_data?.items;
36
+ const meta = d?.media_metadata;
37
+ if (Array.isArray(items) && meta) {
38
+ for (const it of items) {
39
+ const m = it && meta[it.media_id];
40
+ const u = m?.s?.u;
41
+ if (u) gallery_urls.push(decodeHtml(u));
42
+ }
43
+ }
44
+ return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
45
+ }
20
46
  const sub = \${{ args.subreddit | json }};
21
47
  const path = sub ? '/r/' + sub + '/hot.json' : '/hot.json';
22
48
  const limit = \${{ args.limit }};
@@ -32,6 +58,7 @@ cli({
32
58
  author: c.data.author,
33
59
  postId: c.data.id,
34
60
  url: 'https://www.reddit.com' + c.data.permalink,
61
+ ...extractRedditMedia(c.data),
35
62
  }));
36
63
  })()
37
64
  ` },
@@ -44,6 +71,10 @@ cli({
44
71
  postId: '${{ item.postId }}',
45
72
  author: '${{ item.author }}',
46
73
  url: '${{ item.url }}',
74
+ post_hint: '${{ item.post_hint }}',
75
+ url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
76
+ preview_image_url: '${{ item.preview_image_url }}',
77
+ gallery_urls: '${{ item.gallery_urls }}',
47
78
  } },
48
79
  { limit: '${{ args.limit }}' },
49
80
  ],
@@ -6,7 +6,10 @@ describe('reddit hot adapter', () => {
6
6
  const command = getRegistry().get('reddit/hot');
7
7
 
8
8
  it('registers postId, author, and url columns in the hot-list shape', () => {
9
- expect(command?.columns).toEqual(['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url']);
9
+ expect(command?.columns).toEqual([
10
+ 'rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url',
11
+ 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
12
+ ]);
10
13
  expect(command?.pipeline?.[1]?.evaluate).toContain('postId: c.data.id');
11
14
  expect(command?.pipeline?.[1]?.evaluate).toContain("'https://www.reddit.com' + c.data.permalink");
12
15
  expect(command?.pipeline?.[2]?.map).toMatchObject({
@@ -15,4 +18,15 @@ describe('reddit hot adapter', () => {
15
18
  url: '${{ item.url }}',
16
19
  });
17
20
  });
21
+
22
+ it('surfaces post_hint, url_overridden_by_dest, preview_image_url, gallery_urls via extractRedditMedia', () => {
23
+ expect(command?.pipeline?.[1]?.evaluate).toContain('function extractRedditMedia');
24
+ expect(command?.pipeline?.[1]?.evaluate).toContain('...extractRedditMedia(c.data)');
25
+ expect(command?.pipeline?.[2]?.map).toMatchObject({
26
+ post_hint: '${{ item.post_hint }}',
27
+ url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
28
+ preview_image_url: '${{ item.preview_image_url }}',
29
+ gallery_urls: '${{ item.gallery_urls }}',
30
+ });
31
+ });
18
32
  });