fix(audit): ignore sentinels in thrown errors
Avoid classifying fallback text inside thrown error messages as silent row data.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user