// 最小复现/验证脚本:直接编译 src/lib/stock-api.ts 真实源码, // 调用 getStockBoard(null),复现「Cannot read properties of null (reading 'startsWith')」。 // 无测试框架,node 直接运行:node tests/get-stock-board-repro.mjs import fs from "node:fs"; import path from "node:path"; import Module from "node:module"; import ts from "typescript"; const srcPath = path.resolve("src/lib/stock-api.ts"); const source = fs.readFileSync(srcPath, "utf8"); const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true }, }).outputText; const mod = new Module(srcPath); mod.filename = srcPath; mod.paths = Module._nodeModulePaths(path.dirname(srcPath)); // 拦截 @/lib/api-client 别名导入(getStockBoard 本身不依赖它) const origResolve = Module._resolveFilename; Module._resolveFilename = function (request, ...args) { if (request === "@/lib/api-client") return path.resolve("tests/_stub-api-client.cjs"); return origResolve.call(this, request, ...args); }; try { mod._compile(js, srcPath); } finally { Module._resolveFilename = origResolve; } const { getStockBoard } = mod.exports; // ---- 断言 ---- function assertThrows(fn, label) { try { fn(); console.log(`✗ ${label}: 未抛错`); process.exitCode = 1; } catch (e) { console.log(`✓ ${label}: 抛错 -> ${e.message}`); } } function assertNoThrow(fn, label) { try { const r = fn(); console.log(`✓ ${label}: 未抛错 -> ${JSON.stringify(r)}`); return r; } catch (e) { console.log(`✗ ${label}: 抛错 -> ${e.message}`); process.exitCode = 1; } } // 正常代码 assertNoThrow(() => getStockBoard("600000"), "正常代码 getStockBoard('600000')"); // 崩盘场景:securityCode 为 null(East Money 题材列表实测存在),修复后应兜底返回主板 assertNoThrow(() => getStockBoard(null), "null securityCode"); assertNoThrow(() => getStockBoard(undefined), "undefined securityCode"); // 空串不崩 assertNoThrow(() => getStockBoard(""), "空串 getStockBoard('')");