| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- import assert from 'node:assert/strict'
- import { readFile } from 'node:fs/promises'
- import ts from 'typescript'
- const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/services/articleErrors.ts', import.meta.url)
- let source = await readFile(sourceUrl, 'utf8')
- // `lang` 是 PPT 运行时的响应式对象;测试里换成 identity 桩,直接断言「返回的是哪个 key」,
- // 而不是断言译文 —— 译文归 test-article-reading-i18n.mjs 管,这里只管映射对不对。
- source = source
- // 桩返回 key 本身,于是断言的是「映射到哪个 key」而不是译文。带占位符的那条例外:
- // 它多挂一个 `{n}`,好让「段号有没有被填进去」也能被断言到。
- .replace(
- "import { lang } from '@/main'",
- "const lang = new Proxy({}, { get: (_, k) => k === 'ssArticleErrDemoAudio' ? 'ssArticleErrDemoAudio:{n}' : String(k) })",
- )
- .replace(/import \{[^}]*\} from '\.\/articleReading'/, 'class ArticleApiError extends Error { constructor(m, s) { super(m); this.status = s } }')
- const compiled = ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
- }).outputText
- const { friendlyArticleError, articleActiveCountFromError } = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
- const cases = [
- ['Config not found', 'ssArticleErrConfigNotFound'],
- ['Config is not an article config', 'ssArticleErrNotArticleConfig'],
- ['Article content is empty', 'ssArticleErrContentEmpty'],
- ['Session not found', 'ssArticleErrSessionNotFound'],
- ['Paragraph not found', 'ssArticleErrParagraphNotFound'],
- ['Session is abandoned', 'ssArticleSessionExpired'],
- ['Session has ended', 'ssArticleErrSessionEnded'],
- ['Report already generated', 'ssArticleErrReportGenerated'],
- ['Segmentation failed', 'ssArticleSegmentFailed'],
- ['Segmentation returned empty output', 'ssArticleSegmentFailed'],
- ['content is required', 'ssArticleErrBadRequest'],
- ['configId is required', 'ssArticleErrBadRequest'],
- ['userId is required', 'ssArticleErrBadRequest'],
- ['users is required', 'ssArticleErrBadRequest'],
- ['users capped at 100', 'ssArticleErrBadRequest'],
- ]
- for (const [detail, key] of cases) {
- assert.equal(friendlyArticleError(new Error(detail)), key, `bare detail: ${detail}`)
- }
- // request() 万一没拆干净、或者别的 service 抛了带包装的 message,也要吃得下
- assert.equal(
- friendlyArticleError(new Error('{"detail":"Session not found"}')),
- 'ssArticleErrSessionNotFound',
- 'raw JSON body',
- )
- assert.equal(
- friendlyArticleError(new Error('[404] {"detail":"Config not found"}')),
- 'ssArticleErrConfigNotFound',
- '@/services/speaking parse() 的 `[404] {...}` 形状',
- )
- // 409「有人在练」:人数照旧要能摘出来(弹框靠它),但它**不该**被当成错误文案 ——
- // 2026-07-24 起那是问句:handleApply 截住它去弹确认框(spec §4.3 B)。真漏到 toast 只能是
- // 别处误用,那就走通用兜底,绝不出现「暂时无法修改」这种已经不成立的话。
- assert.equal(
- friendlyArticleError(new Error('Students are practising: 3')),
- 'ssArticleErrGeneric',
- '409 不再映射成一句「无法修改」',
- )
- assert.equal(articleActiveCountFromError(new Error('Students are practising: 3')), 3)
- assert.equal(articleActiveCountFromError(new Error('Students are practising: 0')), 0)
- assert.equal(articleActiveCountFromError(new Error('Session not found')), null)
- assert.equal(articleActiveCountFromError(null), null)
- // 保存配置时整篇示范音频同步合成,失败的 detail 带着段号(`[2]` / `[1, 3]`)。
- // 段号是老师唯一能据以行动的信息,必须填进文案,不能只回一句笼统的失败。
- assert.equal(
- friendlyArticleError(new Error('Demo audio synthesis failed: [2]')),
- 'ssArticleErrDemoAudio:2',
- 'single failing paragraph',
- )
- assert.equal(
- friendlyArticleError(new Error('Demo audio synthesis failed: [1, 3]')),
- 'ssArticleErrDemoAudio:1、3',
- 'several failing paragraphs',
- )
- assert.equal(
- friendlyArticleError(new Error('{"detail":"Demo audio synthesis failed: [4]"}')),
- 'ssArticleErrDemoAudio:4',
- 'wrapped in a JSON body',
- )
- // 兜底:绝不把原始内容漏出去
- const leaks = [
- new Error('{"detail":"some brand new backend error"}'),
- new Error('TypeError: Failed to fetch'),
- new Error(''),
- 'a bare string',
- null,
- undefined,
- { nope: true },
- ]
- for (const cause of leaks) {
- const out = friendlyArticleError(cause)
- assert.equal(out, 'ssArticleErrGeneric', `fallback for ${JSON.stringify(String(cause))}`)
- assert.ok(!out.includes('detail'), 'never leak the response body')
- assert.ok(!out.includes('{'), 'never leak JSON')
- }
- console.log(`article errors: ${cases.length + 2} mappings + ${leaks.length} fallbacks OK`)
|