|
|
@@ -0,0 +1,120 @@
|
|
|
+/**
|
|
|
+ * bookScale 优先级回归测试
|
|
|
+ *
|
|
|
+ * 验证 generate 端点的 bookScale 选择逻辑符合新优先级:
|
|
|
+ * 1) body.bookScale(用户本次显式指定)
|
|
|
+ * 2) book.bookScale(已有规模,重新生成时沿用,不再让 AI 覆盖)
|
|
|
+ * 3) AI 自动检测
|
|
|
+ * 4) 兜底 '1000'
|
|
|
+ *
|
|
|
+ * 用 mock 替身模拟 autoDetectBookType 与 BOOK_TYPE_CONFIG,跑四类场景。
|
|
|
+ */
|
|
|
+import { BOOK_TYPE_CONFIG } from '../book-type-config';
|
|
|
+
|
|
|
+interface Book {
|
|
|
+ title: string;
|
|
|
+ description: string;
|
|
|
+ bookScale: string | null | undefined;
|
|
|
+}
|
|
|
+
|
|
|
+/** 模拟修复后的 generate 端点 bookScale 选择逻辑(与 langgraph-controller.ts 一致) */
|
|
|
+async function pickBookScale(
|
|
|
+ body: { bookScale?: string },
|
|
|
+ book: Book,
|
|
|
+ autoDetectBookType: (title: string, description: string) => Promise<string>,
|
|
|
+): Promise<string> {
|
|
|
+ let bookScale: string | undefined;
|
|
|
+
|
|
|
+ if (body.bookScale && BOOK_TYPE_CONFIG[body.bookScale]) {
|
|
|
+ bookScale = body.bookScale;
|
|
|
+ } else if (book.bookScale && BOOK_TYPE_CONFIG[book.bookScale]) {
|
|
|
+ bookScale = book.bookScale;
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ bookScale = await autoDetectBookType(book.title || '', book.description || '');
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ if (!bookScale || !BOOK_TYPE_CONFIG[bookScale]) {
|
|
|
+ bookScale = '1000';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return bookScale;
|
|
|
+}
|
|
|
+
|
|
|
+interface Case {
|
|
|
+ name: string;
|
|
|
+ body: { bookScale?: string };
|
|
|
+ book: Book;
|
|
|
+ mockAiReturn: string;
|
|
|
+ expected: string;
|
|
|
+}
|
|
|
+
|
|
|
+const cases: Case[] = [
|
|
|
+ {
|
|
|
+ name: 'CASE 1: body 显式指定优先于 bookScale',
|
|
|
+ body: { bookScale: '12000' },
|
|
|
+ book: { title: '认知之旅', description: '认知心理学', bookScale: '31000' },
|
|
|
+ mockAiReturn: '50000',
|
|
|
+ expected: '12000',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'CASE 2: 重新生成(无 body),沿用 book.bookScale,不调用 AI',
|
|
|
+ body: {},
|
|
|
+ book: { title: '认知之旅', description: '认知心理学', bookScale: '12000' },
|
|
|
+ mockAiReturn: '1000', // 即使 AI 想返回 1000,也不该被采用
|
|
|
+ expected: '12000',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'CASE 3: 首次生成(book 无 bookScale + 无 body),走 AI',
|
|
|
+ body: {},
|
|
|
+ book: { title: '新书', description: '儿童故事', bookScale: null },
|
|
|
+ mockAiReturn: '7000',
|
|
|
+ expected: '7000',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'CASE 4: 首次生成 + AI 返回 1000(描述无字数),使用 AI 返回值',
|
|
|
+ body: {},
|
|
|
+ book: { title: '新书', description: '短文一篇', bookScale: null },
|
|
|
+ mockAiReturn: '1000',
|
|
|
+ expected: '1000',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'CASE 5: AI 抛错 + book 无 bookScale,兜底 1000',
|
|
|
+ body: {},
|
|
|
+ book: { title: '新书', description: '描述', bookScale: null },
|
|
|
+ mockAiReturn: '__THROW__',
|
|
|
+ expected: '1000',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'CASE 6: book.bookScale 非法值(如 99999),回退到 AI',
|
|
|
+ body: {},
|
|
|
+ book: { title: '异常', description: '描述', bookScale: '99999' },
|
|
|
+ mockAiReturn: '7000',
|
|
|
+ expected: '7000',
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+async function run() {
|
|
|
+ let pass = 0;
|
|
|
+ let fail = 0;
|
|
|
+ for (const c of cases) {
|
|
|
+ const autoDetect = async (_t: string, _d: string) => {
|
|
|
+ if (c.mockAiReturn === '__THROW__') throw new Error('AI 异常');
|
|
|
+ return c.mockAiReturn;
|
|
|
+ };
|
|
|
+ const got = await pickBookScale(c.body, c.book, autoDetect);
|
|
|
+ const ok = got === c.expected;
|
|
|
+ console.log(`${ok ? '✅' : '❌'} ${c.name}`);
|
|
|
+ console.log(` 期望=${c.expected}, 实际=${got}`);
|
|
|
+ if (ok) pass++; else fail++;
|
|
|
+ }
|
|
|
+ console.log(`\n总计 ${pass}/${pass + fail} 通过`);
|
|
|
+ if (fail > 0) process.exit(1);
|
|
|
+}
|
|
|
+
|
|
|
+run().catch(err => {
|
|
|
+ console.error('测试异常:', err);
|
|
|
+ process.exit(1);
|
|
|
+});
|