Compare commits

...

2 Commits

Author SHA1 Message Date
pi-dal 85f81c87f2 test(e2e): accept current apple podcasts fetch errors 2026-03-28 00:13:45 +08:00
pi-dal 6b2ccbd5b8 fix(ci): stabilize plugin and public command checks 2026-03-28 00:13:45 +08:00
5 changed files with 52 additions and 22 deletions
+29 -2
View File
@@ -38,14 +38,14 @@ describe('apple-podcasts search command', () => {
'https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5',
);
expect(result).toEqual([
{
expect.objectContaining({
id: 42,
title: 'Machine Learning Guide',
author: 'OpenCLI',
episodes: 12,
genre: 'Technology',
url: '',
},
}),
]);
});
});
@@ -55,6 +55,30 @@ describe('apple-podcasts top command', () => {
vi.restoreAllMocks();
});
it('adds a timeout signal to chart fetches', async () => {
const cmd = getRegistry().get('apple-podcasts/top');
expect(cmd?.func).toBeTypeOf('function');
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({
feed: {
results: [
{ id: '100', name: 'Top Show', artistName: 'Host A' },
],
},
}),
});
vi.stubGlobal('fetch', fetchMock);
await cmd!.func!(null as any, { country: 'US', limit: 1 });
const [, options] = fetchMock.mock.calls[0] ?? [];
expect(options).toBeDefined();
expect(options.signal).toBeDefined();
expect(options.signal).toHaveProperty('aborted', false);
});
it('uses the canonical Apple charts host and maps ranked results', async () => {
const cmd = getRegistry().get('apple-podcasts/top');
expect(cmd?.func).toBeTypeOf('function');
@@ -76,6 +100,9 @@ describe('apple-podcasts top command', () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json',
expect.objectContaining({
signal: expect.any(Object),
}),
);
expect(result).toEqual([
{ rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
+4 -1
View File
@@ -3,6 +3,7 @@ import { CliError } from '../../errors.js';
// Apple Marketing Tools RSS API — public, no key required
const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
const CHARTS_TIMEOUT_MS = 15_000;
cli({
site: 'apple-podcasts',
@@ -21,7 +22,9 @@ cli({
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
let resp: Response;
try {
resp = await fetch(url);
resp = await fetch(url, {
signal: AbortSignal.timeout(CHARTS_TIMEOUT_MS),
});
} catch (error: any) {
const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
throw new CliError(
+3 -17
View File
@@ -3,7 +3,7 @@ name: hot
description: V2EX 热门话题
domain: www.v2ex.com
strategy: public
browser: true
browser: false
args:
limit:
@@ -12,22 +12,8 @@ args:
description: Number of topics
pipeline:
- navigate: https://www.v2ex.com/
- evaluate: |
(async () => {
const response = await fetch('/api/topics/hot.json', {
credentials: 'include',
headers: {
accept: 'application/json, text/plain, */*',
'x-requested-with': 'XMLHttpRequest',
},
});
if (!response.ok) {
throw new Error(`V2EX hot API request failed: ${response.status}`);
}
return await response.json();
})()
- fetch:
url: https://www.v2ex.com/api/topics/hot.json
- map:
rank: ${{ index + 1 }}
+4 -1
View File
@@ -60,7 +60,10 @@ describe('plugin management E2E', () => {
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
expect(lock[PLUGIN_NAME]).toBeDefined();
expect(lock[PLUGIN_NAME].commitHash).toBeTruthy();
expect(lock[PLUGIN_NAME].source).toContain('opencli-plugin-hot-digest');
expect(lock[PLUGIN_NAME].source).toMatchObject({
kind: 'git',
});
expect(lock[PLUGIN_NAME].source.url).toContain('opencli-plugin-hot-digest');
expect(lock[PLUGIN_NAME].installedAt).toBeTruthy();
}, 60_000);
+12 -1
View File
@@ -21,7 +21,7 @@ function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean
function isExpectedApplePodcastsRestriction(code: number, stderr: string): boolean {
if (code === 0) return false;
return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr)
return /(?:Error \[FETCH_ERROR\]: )?(Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr)
|| stderr === ''; // timeout killed the process before any output
}
@@ -34,6 +34,17 @@ function isExpectedGoogleRestriction(code: number, stderr: string): boolean {
// Keep old name as alias for existing tests
const isExpectedXiaoyuzhouRestriction = isExpectedChineseSiteRestriction;
describe('public command restriction detectors', () => {
it('treats current Apple Podcasts CliError rendering as an expected restriction', () => {
expect(
isExpectedApplePodcastsRestriction(
1,
'⚠️ Unable to reach Apple Podcasts charts for US\n→ Apple charts may be temporarily unavailable (ECONNRESET). Try again later.\n',
),
).toBe(true);
});
});
describe('public commands E2E', () => {
// ── bloomberg (RSS-backed, browser: false) ──
it('bloomberg main returns structured headline data', async () => {