Skip to content

Commit 40592ea

Browse files
fix(adapters): drop silent-sentinel row fallbacks across Apple Podcasts, Reddit, and Gitee (#1634)
* fix(adapters): drop silent-sentinel row fallbacks across Apple Podcasts, Reddit, and Gitee Continues the audit-baseline cleanup from #1611 (lesswrong) and #1631 (wikipedia / 36kr / xiaoyuzhou / zhihu), and follows the direction set by 7164615 (silent-empty-fallback resolutions across Douyin / Jike / WeRead) and ee54eb8 (ignore sentinels in thrown errors). Replaces silent-sentinel row fallbacks with the empty-string signal so agents can tell apart "field has value Unknown" from "upstream returned no value": - apple-podcasts/search: episodes, genre - reddit/saved: title - reddit/upvoted: title - gitee/search: language, description All four files audited for downstream sentinel checks via `grep -nE "=== ?['\"](Unknown|unknown|-)['\"]"`. None reference the swapped values in control flow (verified against the v2ex/me.js class of regression caught in #1631). Intentionally skipped in this batch (will not flip to empty): - gitee/trending.js:272: downstream `project.description !== '-'` check drives the mergedDescription fallback. Same control-flow sentinel pattern as v2ex/me.js. Stays on baseline. - web/read.js x4: `'-'` lives inside rendered diagnostic lines (`lines.push(...)`), not row fields. Empty would render ` GET /a/b` with a doubled space. UX placeholder. - yollomi/{edit,video}.js x6: `file: '-'`, `size: '-'`, `credits: '-'` are user-facing status rows displayed to humans. Empty would collapse columns visually. - zsxq/dynamics.js: `title: '[${d.action || 'unknown'}]'` is a template-literal-rendered title prefix. Empty would render `[]`. Verified live: `opencli apple-podcasts search "lex fridman" --limit 2` returns populated episodes/genre. `opencli gitee search "vue" --limit 2` returns populated language/description. Baseline shrinks accordingly. * test(adapters): add empty-signal coverage for the cluster-3 sentinel swap Mirrors the cluster-2 test additions, pairing the sentinel value swap in this PR with focused unit tests that mock the upstream to return null / missing fields and assert the row surfaces an empty-string signal instead of the old fabricated '-' / 'unknown' sentinel. Coverage: - clis/apple-podcasts/commands.test.js (+1 case): stubs the iTunes Search response with a result that has collectionId / collectionName / artistName populated but no trackCount and no primaryGenreName. Asserts episodes and genre render as '' (was '-' before this PR). - clis/gitee/search.test.js (new): mocks Gitee's `so.gitee.com/v1/search` fetch with two cases - a hit that has only title + url (no langs, no description), and a hit that has all fields populated. Asserts the missing fields render as '' (was '-' before) and that populated fields pass through verbatim. The reddit/saved and reddit/upvoted changes in this PR live inside a page.evaluate template literal that fetches from reddit.com inside the browser context, so the empty-signal branch is executed inside the page rather than in adapter JS. They are 1-char `|| '-'` -> `|| ''` swaps with no downstream sentinel consumer and the same JS semantics demonstrated by the gitee + apple-podcasts tests above. * chore: rebuild cli-manifest.json to drop stale entries from rebase The previous rebase left a stale linkedin/people-search entry in cli-manifest.json that was carried over from a sibling feature branch. This branch does not include the people-search source file, so the entry was an orphan; CI's build-manifest safety check correctly refused to overwrite it. Regenerating with --allow-removals to drop the orphaned entry, after which a normal `npm run build` is a no-op.
1 parent 942539a commit 40592ea

7 files changed

Lines changed: 93 additions & 56 deletions

File tree

clis/apple-podcasts/commands.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ describe('apple-podcasts search command', () => {
4141
}),
4242
]);
4343
});
44+
it('emits empty-string for missing trackCount and primaryGenreName instead of a sentinel', async () => {
45+
const cmd = getRegistry().get('apple-podcasts/search');
46+
const fetchMock = vi.fn().mockResolvedValue({
47+
ok: true,
48+
json: () => Promise.resolve({
49+
results: [
50+
{
51+
collectionId: 99,
52+
collectionName: 'No-Meta Show',
53+
artistName: 'Anon Host',
54+
collectionViewUrl: 'https://example.com/p/99',
55+
},
56+
],
57+
}),
58+
});
59+
vi.stubGlobal('fetch', fetchMock);
60+
const result = await cmd.func({ query: 'no-meta', limit: 1 });
61+
expect(result[0].episodes).toBe('');
62+
expect(result[0].genre).toBe('');
63+
});
4464
});
4565
describe('apple-podcasts top command', () => {
4666
beforeEach(() => {

clis/apple-podcasts/search.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ cli({
2323
id: p.collectionId,
2424
title: p.collectionName,
2525
author: p.artistName,
26-
episodes: p.trackCount ?? '-',
27-
genre: p.primaryGenreName ?? '-',
26+
episodes: p.trackCount ?? '',
27+
genre: p.primaryGenreName ?? '',
2828
url: p.collectionViewUrl || '',
2929
}));
3030
},

clis/gitee/search.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ cli({
123123
rows.push({
124124
rank: rows.length + 1,
125125
name,
126-
language: normalizeText(getFirstText(fields.langs)) || '-',
127-
description: normalizeText(getFirstText(fields.description)) || '-',
126+
language: normalizeText(getFirstText(fields.langs)) || '',
127+
description: normalizeText(getFirstText(fields.description)) || '',
128128
stars: normalizeStars(fields['count.star']),
129129
url: repoUrl,
130130
});

clis/gitee/search.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { getRegistry } from '@jackwener/opencli/registry';
3+
import './search.js';
4+
5+
function mockGiteeResponse(hits) {
6+
return {
7+
ok: true,
8+
json: () => Promise.resolve({ hits: { hits } }),
9+
};
10+
}
11+
12+
function makePage() {
13+
return {
14+
goto: vi.fn().mockResolvedValue(undefined),
15+
wait: vi.fn().mockResolvedValue(undefined),
16+
};
17+
}
18+
19+
describe('gitee search', () => {
20+
beforeEach(() => {
21+
vi.restoreAllMocks();
22+
});
23+
afterEach(() => {
24+
vi.unstubAllGlobals();
25+
});
26+
27+
it('emits empty-string for missing language / description instead of a sentinel', async () => {
28+
const cmd = getRegistry().get('gitee/search');
29+
expect(cmd?.func).toBeTypeOf('function');
30+
const fetchMock = vi.fn().mockResolvedValue(mockGiteeResponse([
31+
{
32+
fields: {
33+
title: 'someuser/no-meta-repo',
34+
url: 'https://gitee.com/someuser/no-meta-repo',
35+
},
36+
},
37+
]));
38+
vi.stubGlobal('fetch', fetchMock);
39+
const rows = await cmd.func(makePage(), { keyword: 'test', limit: 10 });
40+
expect(rows).toHaveLength(1);
41+
expect(rows[0].name).toBe('someuser/no-meta-repo');
42+
expect(rows[0].language).toBe('');
43+
expect(rows[0].description).toBe('');
44+
});
45+
46+
it('passes through populated language / description verbatim', async () => {
47+
const cmd = getRegistry().get('gitee/search');
48+
const fetchMock = vi.fn().mockResolvedValue(mockGiteeResponse([
49+
{
50+
fields: {
51+
title: 'org/repo-a',
52+
url: 'https://gitee.com/org/repo-a',
53+
langs: 'TypeScript',
54+
description: 'A test repo',
55+
'count.star': '42',
56+
},
57+
},
58+
]));
59+
vi.stubGlobal('fetch', fetchMock);
60+
const rows = await cmd.func(makePage(), { keyword: 'test', limit: 10 });
61+
expect(rows[0].language).toBe('TypeScript');
62+
expect(rows[0].description).toBe('A test repo');
63+
expect(rows[0].stars).toBe('42');
64+
});
65+
});

clis/reddit/saved.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ cli({
3030
});
3131
const d = await res.json();
3232
return (d?.data?.children || []).map(c => ({
33-
title: c.data.title || c.data.body?.slice(0, 100) || '-',
33+
title: c.data.title || c.data.body?.slice(0, 100) || '',
3434
subreddit: c.data.subreddit_name_prefixed || 'r/' + (c.data.subreddit || '?'),
3535
score: c.data.score || 0,
3636
comments: c.data.num_comments || 0,

clis/reddit/upvoted.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ cli({
3030
});
3131
const d = await res.json();
3232
return (d?.data?.children || []).map(c => ({
33-
title: c.data.title || '-',
33+
title: c.data.title || '',
3434
subreddit: c.data.subreddit_name_prefixed || 'r/' + (c.data.subreddit || '?'),
3535
score: c.data.score || 0,
3636
comments: c.data.num_comments || 0,

scripts/typed-error-lint-baseline.json

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -819,15 +819,15 @@
819819
"rule": "silent-clamp",
820820
"command": "zhihu/collection",
821821
"file": "clis/zhihu/collection.js",
822-
"line": 141,
822+
"line": 154,
823823
"text": "const currentFetchLimit = Math.min(pageLimit, requestedLimit - collected.length);",
824824
"occurrence": 0
825825
},
826826
{
827827
"rule": "silent-clamp",
828828
"command": "zhihu/collection",
829829
"file": "clis/zhihu/collection.js",
830-
"line": 130,
830+
"line": 143,
831831
"text": "const pageLimit = Math.min(requestedLimit, 20); // 知乎 API 限制每页最大 20",
832832
"occurrence": 0
833833
},
@@ -855,22 +855,6 @@
855855
"text": "const limit = Math.max(1, Math.min(Number(args.limit) || 10, 25));",
856856
"occurrence": 0
857857
},
858-
{
859-
"rule": "silent-sentinel",
860-
"command": "apple-podcasts/search",
861-
"file": "clis/apple-podcasts/search.js",
862-
"line": 26,
863-
"text": "episodes: p.trackCount ?? '-',",
864-
"occurrence": 0
865-
},
866-
{
867-
"rule": "silent-sentinel",
868-
"command": "apple-podcasts/search",
869-
"file": "clis/apple-podcasts/search.js",
870-
"line": 27,
871-
"text": "genre: p.primaryGenreName ?? '-',",
872-
"occurrence": 0
873-
},
874858
{
875859
"rule": "silent-sentinel",
876860
"command": "bilibili/download",
@@ -887,22 +871,6 @@
887871
"text": "const jobName = f.jobName || 'unknown';",
888872
"occurrence": 0
889873
},
890-
{
891-
"rule": "silent-sentinel",
892-
"command": "gitee/search",
893-
"file": "clis/gitee/search.js",
894-
"line": 127,
895-
"text": "description: normalizeText(getFirstText(fields.description)) || '-',",
896-
"occurrence": 0
897-
},
898-
{
899-
"rule": "silent-sentinel",
900-
"command": "gitee/search",
901-
"file": "clis/gitee/search.js",
902-
"line": 126,
903-
"text": "language: normalizeText(getFirstText(fields.langs)) || '-',",
904-
"occurrence": 0
905-
},
906874
{
907875
"rule": "silent-sentinel",
908876
"command": "gitee/trending",
@@ -935,22 +903,6 @@
935903
"text": "prompt: params.prompt || item.common_attr?.title || 'N/A',",
936904
"occurrence": 0
937905
},
938-
{
939-
"rule": "silent-sentinel",
940-
"command": "reddit/saved",
941-
"file": "clis/reddit/saved.js",
942-
"line": 33,
943-
"text": "title: c.data.title || c.data.body?.slice(0, 100) || '-',",
944-
"occurrence": 0
945-
},
946-
{
947-
"rule": "silent-sentinel",
948-
"command": "reddit/upvoted",
949-
"file": "clis/reddit/upvoted.js",
950-
"line": 33,
951-
"text": "title: c.data.title || '-',",
952-
"occurrence": 0
953-
},
954906
{
955907
"rule": "silent-sentinel",
956908
"command": "twitter/accept",

0 commit comments

Comments
 (0)