fix(audit): ignore sentinels in thrown errors

Avoid classifying fallback text inside thrown error messages as silent row data.
This commit is contained in:
jakevin
2026-05-16 16:51:06 +08:00
committed by GitHub
parent 663b3387ee
commit ee54eb8e62
4 changed files with 44 additions and 96 deletions
+4
View File
@@ -6,6 +6,10 @@
* **adapters** — surface the remaining `silent-empty-fallback` adapter failures as typed errors. Douyin user video comment fetch failures, Jike SSR JSON parse failures, and WeRead search-page fetch failures now throw `CommandExecutionError`; true empty Douyin/Jike/WeRead result sets now throw `EmptyResultError`.
### Internal
* **audit** — stop flagging sentinel fallback strings inside thrown error messages as `silent-sentinel` violations. These are typed failure diagnostics rather than fake row data, reducing the typed-error baseline to actual adapter output fallbacks.
## [1.7.22](https://github.com/jackwener/opencli/compare/v1.7.21...v1.7.22) (2026-05-15)
External CLI ergonomics + two adapter envelope/auth fixes. New `longbridge` external CLI entry; `opencli list` / root help now render human-readable brand labels for executables whose bare name is ambiguous.
-88
View File
@@ -879,22 +879,6 @@
"text": "{ field: 'date', value: data.date || '-' },",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "51job/hot",
"file": "clis/51job/hot.js",
"line": 50,
"text": "throw new CliError('API_ERROR', `51job hot failed: ${data.message ?? 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "51job/search",
"file": "clis/51job/search.js",
"line": 72,
"text": "throw new CliError('API_ERROR', `51job search failed: ${data.message ?? 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "apple-podcasts/search",
@@ -927,46 +911,6 @@
"text": "const jobName = f.jobName || 'unknown';",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "doubao-app/send",
"file": "clis/doubao-app/send.js",
"line": 19,
"text": "throw new Error('Could not find chat input: ' + (injected?.error || 'unknown'));",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "douyin/draft",
"file": "clis/douyin/draft.js",
"line": 288,
"text": "throw new CommandExecutionError('未检测到抖音草稿恢复提示', `当前页面: ${lastState.href || 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "douyin/draft",
"file": "clis/douyin/draft.js",
"line": 210,
"text": "throw new CommandExecutionError('等待抖音封面处理完成超时', lastPanelText || 'unknown');",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "douyin/draft",
"file": "clis/douyin/draft.js",
"line": 57,
"text": "throw new CommandExecutionError('等待抖音草稿编辑页超时', `当前页面: ${lastState.href || 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "douyin/draft",
"file": "clis/douyin/draft.js",
"line": 262,
"text": "throw new CommandExecutionError(`点击草稿按钮失败: ${result?.reason || 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "gitee/search",
@@ -999,14 +943,6 @@
"text": "description: project.description !== '-' ? project.description : (mergedDescription || '-'),",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "instagram/note",
"file": "clis/instagram/note.js",
"line": 219,
"text": "throw new CommandExecutionError(`Instagram note publish failed at ${String(result?.stage || 'unknown')}: ${String(result?.text || 'unknown error')}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "jimeng/history",
@@ -1119,22 +1055,6 @@
"text": "author: item.user?.displayName ?? 'Unknown',",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "linux-do/topic-content",
"file": "clis/linux-do/topic-content.js",
"line": 121,
"text": "throw new CommandExecutionError(result.error || `linux.do request failed: HTTP ${result.status ?? 'unknown'}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "powerchina/search",
"file": "clis/powerchina/search.js",
"line": 149,
"text": "throw new Error(`[taxonomy=relay_unavailable] site=powerchina command=search api code=${data.code ?? 'unknown'} msg=${cleanText(data.msg)}`);",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "reddit/saved",
@@ -1367,14 +1287,6 @@
"text": "result.author = authorEl?.textContent?.trim() || 'unknown';",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "xiaohongshu/publish",
"file": "clis/xiaohongshu/publish.js",
"line": 529,
"text": "throw new Error(`Image injection failed: ${upload.error ?? 'unknown'}. ` +",
"occurrence": 0
},
{
"rule": "silent-sentinel",
"command": "xiaoyuzhou/download",
+24
View File
@@ -249,4 +249,28 @@ describe('convention audit', () => {
expect(violations.map((violation) => violation.command)).toEqual(['demo/catch']);
});
it('does not report sentinel fallbacks inside thrown error messages', () => {
const root = makeProject([
{ site: 'demo', name: 'error', access: 'read', columns: ['id'], sourceFile: 'demo/error.js' },
{ site: 'demo', name: 'row', access: 'read', columns: ['id', 'title'], sourceFile: 'demo/row.js' },
], {
'demo/error.js': `
export async function run(data) {
if (!data.ok) throw new Error(\`demo failed: \${data.message ?? 'unknown'}\`);
return [{ id: 1 }];
}
`,
'demo/row.js': `
export async function run(item) {
return [{ id: item.id, title: item.title ?? 'unknown' }];
}
`,
});
const report = runConventionAudit({ projectRoot: root });
const violations = report.categories.find((item) => item.rule === 'silent-sentinel')!.violations;
expect(violations.map((violation) => violation.command)).toEqual(['demo/row']);
});
});
+16 -8
View File
@@ -322,20 +322,28 @@ function auditTypedErrorPatterns(
}
const sentinel = /(?:\?\?|\|\|)\s*(['"])(unknown|Unknown|UNKNOWN|N\/A|n\/a|NA|未知|-)\1/.exec(line);
if (sentinel) {
violations.push({
rule: 'silent-sentinel',
...command,
file: relative,
line: index + 1,
message: `sentinel fallback ${sentinel[0].trim()} can turn missing data into fake data; prefer dropping the field or throwing a typed error`,
details: { text: line.trim() },
});
if (!isThrowMessageLine(line)) {
violations.push({
rule: 'silent-sentinel',
...command,
file: relative,
line: index + 1,
message: `sentinel fallback ${sentinel[0].trim()} can turn missing data into fake data; prefer dropping the field or throwing a typed error`,
details: { text: line.trim() },
});
}
}
offset += line.length + 1;
});
return dedupeViolations(violations);
}
function isThrowMessageLine(line: string): boolean {
// Only single-line `throw new X(...)` diagnostics are ignored. Multi-line
// throw expressions with row-like sentinel fallbacks still stay visible.
return /\bthrow\s+new\b/.test(line);
}
function auditWriteDeletePair(entries: ManifestCommand[]): ConventionViolation[] {
const bySite = new Map<string, ManifestCommand[]>();
for (const entry of entries) {