From d1285f38bd1a7c243f26a3f6bd6d8b3a07efbbed Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 12:12:13 +0800 Subject: [PATCH 1/7] feat(v2ex): add node, user, member, replies, nodes commands Add 5 new public API commands to the v2ex adapter: - node: browse topics by node name - user: list topics by username - member: show user profile - replies: list topic replies - nodes: list all nodes sorted by topic count All commands use strategy: public, browser: false. --- src/clis/v2ex/member.yaml | 29 +++++++++++++++++++++++++++++ src/clis/v2ex/node.yaml | 33 +++++++++++++++++++++++++++++++++ src/clis/v2ex/nodes.yaml | 31 +++++++++++++++++++++++++++++++ src/clis/v2ex/replies.yaml | 32 ++++++++++++++++++++++++++++++++ src/clis/v2ex/user.yaml | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 src/clis/v2ex/member.yaml create mode 100644 src/clis/v2ex/node.yaml create mode 100644 src/clis/v2ex/nodes.yaml create mode 100644 src/clis/v2ex/replies.yaml create mode 100644 src/clis/v2ex/user.yaml diff --git a/src/clis/v2ex/member.yaml b/src/clis/v2ex/member.yaml new file mode 100644 index 00000000..b2b21a3f --- /dev/null +++ b/src/clis/v2ex/member.yaml @@ -0,0 +1,29 @@ +site: v2ex +name: member +description: V2EX 用户资料 +domain: www.v2ex.com +strategy: public +browser: false + +args: + username: + positional: true + type: str + required: true + description: Username + +pipeline: + - fetch: + url: https://www.v2ex.com/api/members/show.json + params: + username: ${{ args.username }} + + - map: + username: ${{ item.username }} + tagline: ${{ item.tagline }} + website: ${{ item.website }} + github: ${{ item.github }} + twitter: ${{ item.twitter }} + location: ${{ item.location }} + +columns: [username, tagline, website, github, twitter, location] diff --git a/src/clis/v2ex/node.yaml b/src/clis/v2ex/node.yaml new file mode 100644 index 00000000..9fa2c736 --- /dev/null +++ b/src/clis/v2ex/node.yaml @@ -0,0 +1,33 @@ +site: v2ex +name: node +description: V2EX 节点话题列表 +domain: www.v2ex.com +strategy: public +browser: false + +args: + name: + positional: true + type: str + required: true + description: Node name (e.g. python, javascript, apple) + limit: + type: int + default: 10 + description: Number of topics + +pipeline: + - fetch: + url: https://www.v2ex.com/api/topics/show.json + params: + node_name: ${{ args.name }} + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + author: ${{ item.member.username }} + replies: ${{ item.replies }} + + - limit: ${{ args.limit }} + +columns: [rank, title, author, replies] diff --git a/src/clis/v2ex/nodes.yaml b/src/clis/v2ex/nodes.yaml new file mode 100644 index 00000000..534a7696 --- /dev/null +++ b/src/clis/v2ex/nodes.yaml @@ -0,0 +1,31 @@ +site: v2ex +name: nodes +description: V2EX 所有节点列表 +domain: www.v2ex.com +strategy: public +browser: false + +args: + limit: + type: int + default: 30 + description: Number of nodes + +pipeline: + - fetch: + url: https://www.v2ex.com/api/nodes/all.json + + - sort: + by: topics + order: desc + + - map: + rank: ${{ index + 1 }} + name: ${{ item.name }} + title: ${{ item.title }} + topics: ${{ item.topics }} + stars: ${{ item.stars }} + + - limit: ${{ args.limit }} + +columns: [rank, name, title, topics, stars] diff --git a/src/clis/v2ex/replies.yaml b/src/clis/v2ex/replies.yaml new file mode 100644 index 00000000..deb24346 --- /dev/null +++ b/src/clis/v2ex/replies.yaml @@ -0,0 +1,32 @@ +site: v2ex +name: replies +description: V2EX 主题回复列表 +domain: www.v2ex.com +strategy: public +browser: false + +args: + id: + positional: true + type: str + required: true + description: Topic ID + limit: + type: int + default: 20 + description: Number of replies + +pipeline: + - fetch: + url: https://www.v2ex.com/api/replies/show.json + params: + topic_id: ${{ args.id }} + + - map: + floor: ${{ index + 1 }} + author: ${{ item.member.username }} + content: ${{ item.content }} + + - limit: ${{ args.limit }} + +columns: [floor, author, content] diff --git a/src/clis/v2ex/user.yaml b/src/clis/v2ex/user.yaml new file mode 100644 index 00000000..dff7ce3b --- /dev/null +++ b/src/clis/v2ex/user.yaml @@ -0,0 +1,33 @@ +site: v2ex +name: user +description: V2EX 用户发帖列表 +domain: www.v2ex.com +strategy: public +browser: false + +args: + username: + positional: true + type: str + required: true + description: Username + limit: + type: int + default: 10 + description: Number of topics + +pipeline: + - fetch: + url: https://www.v2ex.com/api/topics/show.json + params: + username: ${{ args.username }} + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + node: ${{ item.node.title }} + replies: ${{ item.replies }} + + - limit: ${{ args.limit }} + +columns: [rank, title, node, replies] From dc6788ca31f20d682466013513ba32ae64f05acd Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 12:18:42 +0800 Subject: [PATCH 2/7] test(v2ex): add E2E tests for node, user, member, replies, nodes commands --- tests/e2e/public-commands.test.ts | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/e2e/public-commands.test.ts b/tests/e2e/public-commands.test.ts index 2790cc16..775bc164 100644 --- a/tests/e2e/public-commands.test.ts +++ b/tests/e2e/public-commands.test.ts @@ -239,6 +239,56 @@ describe('public commands E2E', () => { } }, 30_000); + it('v2ex node returns topics for a given node', async () => { + const { stdout, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); + expect(code).toBe(0); + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('author'); + }, 30_000); + + it('v2ex user returns topics by username', async () => { + const { stdout, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); + expect(code).toBe(0); + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('node'); + }, 30_000); + + it('v2ex member returns user profile', async () => { + const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); + expect(code).toBe(0); + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(1); + expect(data[0]).toHaveProperty('username'); + }, 30_000); + + it('v2ex replies returns topic replies', async () => { + const { stdout, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); + expect(code).toBe(0); + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('author'); + expect(data[0]).toHaveProperty('content'); + }, 30_000); + + it('v2ex nodes returns node list sorted by topics', async () => { + const { stdout, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); + expect(code).toBe(0); + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(5); + expect(data[0]).toHaveProperty('name'); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('topics'); + }, 30_000); + // ── xiaoyuzhou (Chinese site — may return empty on overseas CI runners) ── it('xiaoyuzhou podcast returns podcast profile', async () => { const { stdout, stderr, code } = await runCli(['xiaoyuzhou', 'podcast', '6013f9f58e2f7ee375cf4216', '-f', 'json']); From 2db80f100f9238285b925fb9bbd04cf61218b56f Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 12:23:34 +0800 Subject: [PATCH 3/7] docs(v2ex): update adapter docs with new commands --- docs/adapters/browser/v2ex.md | 34 ++++++++++++++++++++++++---------- docs/adapters/index.md | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/adapters/browser/v2ex.md b/docs/adapters/browser/v2ex.md index 85f1d72b..e7c7a38d 100644 --- a/docs/adapters/browser/v2ex.md +++ b/docs/adapters/browser/v2ex.md @@ -6,24 +6,38 @@ | Command | Description | |---------|-------------| -| `opencli v2ex hot` | | -| `opencli v2ex latest` | | -| `opencli v2ex topic` | | -| `opencli v2ex daily` | | -| `opencli v2ex me` | | -| `opencli v2ex notifications` | | +| `opencli v2ex hot` | Hot topics | +| `opencli v2ex latest` | Latest topics | +| `opencli v2ex topic ` | Topic detail | +| `opencli v2ex node ` | Topics by node | +| `opencli v2ex user ` | Topics by user | +| `opencli v2ex member ` | User profile | +| `opencli v2ex replies ` | Topic replies | +| `opencli v2ex nodes` | All nodes (sorted by topic count) | +| `opencli v2ex daily` | Daily hot | +| `opencli v2ex me` | My profile (auth required) | +| `opencli v2ex notifications` | My notifications (auth required) | ## Usage Examples ```bash -# Quick start +# Hot topics opencli v2ex hot --limit 5 +# Browse topics in a node +opencli v2ex node python + +# View topic replies +opencli v2ex replies 1000 + +# User profile +opencli v2ex member Livid + +# List all nodes +opencli v2ex nodes --limit 10 + # JSON output opencli v2ex hot -f json - -# Verbose mode -opencli v2ex hot -v ``` ## Prerequisites diff --git a/docs/adapters/index.md b/docs/adapters/index.md index dfce4633..1f24ae86 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -13,7 +13,7 @@ Run `opencli list` for the live registry. | **[xiaohongshu](/adapters/browser/xiaohongshu)** | `search` `notifications` `feed` `me` `user` `download` `publish` | 🔐 Browser | | **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 🔐 Browser | | **[youtube](/adapters/browser/youtube)** | `search` `video` `transcript` | 🔐 Browser | -| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `daily` `me` `notifications` | 🌐 / 🔐 | +| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 | | **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 | | **[weibo](/adapters/browser/weibo)** | `hot` | 🔐 Browser | | **[linkedin](/adapters/browser/linkedin)** | `search` | 🔐 Browser | From 528570d2e291d06cd29459266032eb4123b635c5 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 12:32:01 +0800 Subject: [PATCH 4/7] fix(v2ex): address review findings - rate-limit guards, sort verification, docs --- docs/adapters/browser/v2ex.md | 4 ++++ src/clis/v2ex/node.yaml | 2 +- src/clis/v2ex/user.yaml | 2 +- tests/e2e/public-commands.test.ts | 32 ++++++++++++++++++++++++++----- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/adapters/browser/v2ex.md b/docs/adapters/browser/v2ex.md index e7c7a38d..018074ff 100644 --- a/docs/adapters/browser/v2ex.md +++ b/docs/adapters/browser/v2ex.md @@ -42,5 +42,9 @@ opencli v2ex hot -f json ## Prerequisites +Most commands (`hot`, `latest`, `topic`, `node`, `user`, `member`, `replies`, `nodes`) use the public V2EX API and **require no browser or login**. + +For `daily`, `me`, and `notifications`: + - Chrome running and **logged into** v2ex.com - [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/src/clis/v2ex/node.yaml b/src/clis/v2ex/node.yaml index 9fa2c736..174afbac 100644 --- a/src/clis/v2ex/node.yaml +++ b/src/clis/v2ex/node.yaml @@ -14,7 +14,7 @@ args: limit: type: int default: 10 - description: Number of topics + description: Number of topics (API returns max 20) pipeline: - fetch: diff --git a/src/clis/v2ex/user.yaml b/src/clis/v2ex/user.yaml index dff7ce3b..e37ed55d 100644 --- a/src/clis/v2ex/user.yaml +++ b/src/clis/v2ex/user.yaml @@ -14,7 +14,7 @@ args: limit: type: int default: 10 - description: Number of topics + description: Number of topics (API returns max 20) pipeline: - fetch: diff --git a/tests/e2e/public-commands.test.ts b/tests/e2e/public-commands.test.ts index 775bc164..052dda9e 100644 --- a/tests/e2e/public-commands.test.ts +++ b/tests/e2e/public-commands.test.ts @@ -240,7 +240,11 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex node returns topics for a given node', async () => { - const { stdout, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); + const { stdout, stderr, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); + if (isExpectedChineseSiteRestriction(code, stderr)) { + console.warn(`v2ex node skipped: ${stderr.trim()}`); + return; + } expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); @@ -250,7 +254,11 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex user returns topics by username', async () => { - const { stdout, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); + const { stdout, stderr, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); + if (isExpectedChineseSiteRestriction(code, stderr)) { + console.warn(`v2ex user skipped: ${stderr.trim()}`); + return; + } expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); @@ -260,7 +268,11 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex member returns user profile', async () => { - const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); + const { stdout, stderr, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); + if (isExpectedChineseSiteRestriction(code, stderr)) { + console.warn(`v2ex member skipped: ${stderr.trim()}`); + return; + } expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); @@ -269,7 +281,11 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex replies returns topic replies', async () => { - const { stdout, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); + const { stdout, stderr, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); + if (isExpectedChineseSiteRestriction(code, stderr)) { + console.warn(`v2ex replies skipped: ${stderr.trim()}`); + return; + } expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); @@ -279,7 +295,11 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex nodes returns node list sorted by topics', async () => { - const { stdout, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); + const { stdout, stderr, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); + if (isExpectedChineseSiteRestriction(code, stderr)) { + console.warn(`v2ex nodes skipped: ${stderr.trim()}`); + return; + } expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); @@ -287,6 +307,8 @@ describe('public commands E2E', () => { expect(data[0]).toHaveProperty('name'); expect(data[0]).toHaveProperty('title'); expect(data[0]).toHaveProperty('topics'); + // Verify descending sort by topic count + expect(Number(data[0].topics)).toBeGreaterThanOrEqual(Number(data[data.length - 1].topics)); }, 30_000); // ── xiaoyuzhou (Chinese site — may return empty on overseas CI runners) ── From 1dacf79ccb694b898854a83ed597e8811543d557 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 12:36:22 +0800 Subject: [PATCH 5/7] docs(v2ex): update README command tables and add user example --- README.md | 2 +- README.zh-CN.md | 2 +- docs/adapters/browser/v2ex.md | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dac94d11..c2e26489 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Run `opencli list` for the live registry. | **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` | Desktop | | **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | Desktop | | **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | Desktop | -| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | Public / Browser | +| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | Public / Browser | | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | Browser | | **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | Desktop | | **chatgpt** | `status` `new` `send` `read` `ask` | Desktop | diff --git a/README.zh-CN.md b/README.zh-CN.md index 198cc416..720b1caf 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -111,7 +111,7 @@ npm install -g @jackwener/opencli@latest | **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` | 桌面端 | | **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | 桌面端 | | **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | 桌面端 | -| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 公开 / 浏览器 | +| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 公开 / 浏览器 | | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | 浏览器 | | **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | 桌面端 | | **chatgpt** | `status` `new` `send` `read` `ask` | 桌面端 | diff --git a/docs/adapters/browser/v2ex.md b/docs/adapters/browser/v2ex.md index 018074ff..6a4b441d 100644 --- a/docs/adapters/browser/v2ex.md +++ b/docs/adapters/browser/v2ex.md @@ -30,6 +30,9 @@ opencli v2ex node python # View topic replies opencli v2ex replies 1000 +# User's topics +opencli v2ex user Livid + # User profile opencli v2ex member Livid From 2bde5690ec1da79d5d358f5c7a6bcc684789f164 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Mon, 23 Mar 2026 15:35:20 +0800 Subject: [PATCH 6/7] test(v2ex): improve test quality - soft guards, value assertions, smoke tests - Replace isExpectedChineseSiteRestriction with if(code===0) soft guard (V2EX is globally accessible; YAML fetch doesn't throw FETCH_ERROR) - Add value assertions: member username===Livid, limit effectiveness - Add smoke tests for node, member, replies, nodes commands --- tests/e2e/public-commands.test.ts | 93 ++++++++++++++----------------- tests/smoke/api-health.test.ts | 61 ++++++++++++++++++-- 2 files changed, 96 insertions(+), 58 deletions(-) diff --git a/tests/e2e/public-commands.test.ts b/tests/e2e/public-commands.test.ts index 052dda9e..2953e47b 100644 --- a/tests/e2e/public-commands.test.ts +++ b/tests/e2e/public-commands.test.ts @@ -240,75 +240,64 @@ describe('public commands E2E', () => { }, 30_000); it('v2ex node returns topics for a given node', async () => { - const { stdout, stderr, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); - if (isExpectedChineseSiteRestriction(code, stderr)) { - console.warn(`v2ex node skipped: ${stderr.trim()}`); - return; + const { stdout, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); + // V2EX may rate-limit; only assert when successful + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data.length).toBeLessThanOrEqual(3); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('author'); } - expect(code).toBe(0); - const data = parseJsonOutput(stdout); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBeGreaterThanOrEqual(1); - expect(data[0]).toHaveProperty('title'); - expect(data[0]).toHaveProperty('author'); }, 30_000); it('v2ex user returns topics by username', async () => { - const { stdout, stderr, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); - if (isExpectedChineseSiteRestriction(code, stderr)) { - console.warn(`v2ex user skipped: ${stderr.trim()}`); - return; + const { stdout, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data.length).toBeLessThanOrEqual(3); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('node'); } - expect(code).toBe(0); - const data = parseJsonOutput(stdout); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBeGreaterThanOrEqual(1); - expect(data[0]).toHaveProperty('title'); - expect(data[0]).toHaveProperty('node'); }, 30_000); it('v2ex member returns user profile', async () => { - const { stdout, stderr, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); - if (isExpectedChineseSiteRestriction(code, stderr)) { - console.warn(`v2ex member skipped: ${stderr.trim()}`); - return; + const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(1); + expect(data[0].username).toBe('Livid'); } - expect(code).toBe(0); - const data = parseJsonOutput(stdout); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(1); - expect(data[0]).toHaveProperty('username'); }, 30_000); it('v2ex replies returns topic replies', async () => { - const { stdout, stderr, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); - if (isExpectedChineseSiteRestriction(code, stderr)) { - console.warn(`v2ex replies skipped: ${stderr.trim()}`); - return; + const { stdout, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data.length).toBeLessThanOrEqual(3); + expect(data[0]).toHaveProperty('author'); + expect(data[0]).toHaveProperty('content'); } - expect(code).toBe(0); - const data = parseJsonOutput(stdout); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBeGreaterThanOrEqual(1); - expect(data[0]).toHaveProperty('author'); - expect(data[0]).toHaveProperty('content'); }, 30_000); it('v2ex nodes returns node list sorted by topics', async () => { - const { stdout, stderr, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); - if (isExpectedChineseSiteRestriction(code, stderr)) { - console.warn(`v2ex nodes skipped: ${stderr.trim()}`); - return; + const { stdout, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(5); + expect(data[0]).toHaveProperty('name'); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('topics'); + // Verify descending sort by topic count + expect(Number(data[0].topics)).toBeGreaterThanOrEqual(Number(data[data.length - 1].topics)); } - expect(code).toBe(0); - const data = parseJsonOutput(stdout); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(5); - expect(data[0]).toHaveProperty('name'); - expect(data[0]).toHaveProperty('title'); - expect(data[0]).toHaveProperty('topics'); - // Verify descending sort by topic count - expect(Number(data[0].topics)).toBeGreaterThanOrEqual(Number(data[data.length - 1].topics)); }, 30_000); // ── xiaoyuzhou (Chinese site — may return empty on overseas CI runners) ── diff --git a/tests/smoke/api-health.test.ts b/tests/smoke/api-health.test.ts index d83b46f6..8e6e2ba1 100644 --- a/tests/smoke/api-health.test.ts +++ b/tests/smoke/api-health.test.ts @@ -4,11 +4,10 @@ * These verify that external APIs haven't changed their structure. */ -import { describe, it, expect } from 'vitest'; -import { runCli, parseJsonOutput } from '../e2e/helpers.js'; +import { describe, expect, it } from 'vitest'; +import { parseJsonOutput, runCli } from '../e2e/helpers.js'; describe('API health smoke tests', () => { - // ── Public API commands (should always work) ── it('hackernews API is responsive and returns expected structure', async () => { const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '5', '-f', 'json']); @@ -46,6 +45,43 @@ describe('API health smoke tests', () => { } }, 30_000); + it('v2ex node API is responsive', async () => { + const { stdout, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('author'); + } + }, 30_000); + + it('v2ex member API is responsive', async () => { + const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(data.length).toBe(1); + expect(data[0].username).toBe('Livid'); + } + }, 30_000); + + it('v2ex replies API is responsive', async () => { + const { stdout, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('author'); + } + }, 30_000); + + it('v2ex nodes API is responsive', async () => { + const { stdout, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(data.length).toBe(5); + expect(data[0]).toHaveProperty('topics'); + } + }, 30_000); + // ── Validate all adapters ── it('all adapter definitions are valid', async () => { const { stdout, code } = await runCli(['validate']); @@ -61,9 +97,22 @@ describe('API health smoke tests', () => { const sites = new Set(data.map((d: any) => d.site)); // Verify all 17 sites are present for (const expected of [ - 'hackernews', 'bbc', 'bilibili', 'v2ex', 'weibo', 'zhihu', - 'twitter', 'reddit', 'xueqiu', 'reuters', 'youtube', - 'smzdm', 'boss', 'ctrip', 'coupang', 'xiaohongshu', + 'hackernews', + 'bbc', + 'bilibili', + 'v2ex', + 'weibo', + 'zhihu', + 'twitter', + 'reddit', + 'xueqiu', + 'reuters', + 'youtube', + 'smzdm', + 'boss', + 'ctrip', + 'coupang', + 'xiaohongshu', 'yahoo-finance', ]) { expect(sites.has(expected)).toBe(true); From b4039e4051aaaa8986b08b0db8500294add5f522 Mon Sep 17 00:00:00 2001 From: jackwener Date: Mon, 23 Mar 2026 17:44:45 +0800 Subject: [PATCH 7/7] fix(v2ex): add url field to node/user commands, add missing user smoke test - Add url to node.yaml and user.yaml pipeline map steps and columns (V2EX API provides item.url; improves usability for follow-up lookups) - Add v2ex user smoke test (other 4 new commands all had smoke tests; user was missing) - Update E2E assertions to verify url field in node/user results --- src/clis/v2ex/node.yaml | 3 ++- src/clis/v2ex/user.yaml | 3 ++- tests/e2e/public-commands.test.ts | 2 ++ tests/smoke/api-health.test.ts | 10 ++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/clis/v2ex/node.yaml b/src/clis/v2ex/node.yaml index 174afbac..8c26b6de 100644 --- a/src/clis/v2ex/node.yaml +++ b/src/clis/v2ex/node.yaml @@ -27,7 +27,8 @@ pipeline: title: ${{ item.title }} author: ${{ item.member.username }} replies: ${{ item.replies }} + url: ${{ item.url }} - limit: ${{ args.limit }} -columns: [rank, title, author, replies] +columns: [rank, title, author, replies, url] diff --git a/src/clis/v2ex/user.yaml b/src/clis/v2ex/user.yaml index e37ed55d..b60dd485 100644 --- a/src/clis/v2ex/user.yaml +++ b/src/clis/v2ex/user.yaml @@ -27,7 +27,8 @@ pipeline: title: ${{ item.title }} node: ${{ item.node.title }} replies: ${{ item.replies }} + url: ${{ item.url }} - limit: ${{ args.limit }} -columns: [rank, title, node, replies] +columns: [rank, title, node, replies, url] diff --git a/tests/e2e/public-commands.test.ts b/tests/e2e/public-commands.test.ts index 2953e47b..2de6da5e 100644 --- a/tests/e2e/public-commands.test.ts +++ b/tests/e2e/public-commands.test.ts @@ -249,6 +249,7 @@ describe('public commands E2E', () => { expect(data.length).toBeLessThanOrEqual(3); expect(data[0]).toHaveProperty('title'); expect(data[0]).toHaveProperty('author'); + expect(data[0]).toHaveProperty('url'); } }, 30_000); @@ -261,6 +262,7 @@ describe('public commands E2E', () => { expect(data.length).toBeLessThanOrEqual(3); expect(data[0]).toHaveProperty('title'); expect(data[0]).toHaveProperty('node'); + expect(data[0]).toHaveProperty('url'); } }, 30_000); diff --git a/tests/smoke/api-health.test.ts b/tests/smoke/api-health.test.ts index 8e6e2ba1..5f0295ef 100644 --- a/tests/smoke/api-health.test.ts +++ b/tests/smoke/api-health.test.ts @@ -55,6 +55,16 @@ describe('API health smoke tests', () => { } }, 30_000); + it('v2ex user API is responsive', async () => { + const { stdout, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']); + if (code === 0) { + const data = parseJsonOutput(stdout); + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data[0]).toHaveProperty('title'); + expect(data[0]).toHaveProperty('url'); + } + }, 30_000); + it('v2ex member API is responsive', async () => { const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']); if (code === 0) {