import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8'); const functions = source.slice(source.indexOf('function importCell('), source.indexOf('async function commitVocabularyImport(')); function importer(nodes = [{ nodeId: 42, nodeName: '第一单元', bookName: '真实教材', stageName: '小学' }], existing = null) { const queries = []; class Batch { constructor() { this.id = 'batch'; this.createdAt = new Date(); } set() {} async save() {} } const context = vm.createContext({ Parse: { Object: Batch }, Psql: { query: async () => nodes, oneOrNone: async (sql, args) => { queries.push({ sql, args }); return existing; } }, text: value => String(value ?? '').trim(), number: value => Number(value) || 0, requireRole() {}, reasonOf: () => 'test', companyIdOf: () => 'tenant', companyPointer: () => ({}), fail: (_status, message) => { throw new Error(message); }, randomObjectId: () => 'random', audit: async () => {}, dateIso: value => value.toISOString(), legacyVocabularyMedia: value => String(value ?? '').replace('https://a018.2018.z01.com/UploadFiles/', ''), }); vm.runInContext(functions, context); return { queries, run: async (rows, targetNodeId) => JSON.parse(JSON.stringify(await context.previewVocabularyImport({ current: { id: 'operator' } }, {}, { rows, ...(targetNodeId === undefined ? {} : { targetNodeId }) }))) }; } test('old template headers preserve GeneralID and exported audio URLs', async () => { const { run, queries } = importer(undefined, { objectId: 'word', title: 'time', nodeId: 42, sy: '时间', yp: 'time.mp3' }); const result = await run([{ ' GeneralID(可选) ': '123', 单元ID: '42', 单词: 'time', 释义: '时间', 音频URL: 'https://a018.2018.z01.com/UploadFiles/time.mp3' }]); assert.equal(result.summary.unchanged, 1); assert.equal(queries[0].args[1], '123'); assert.match(queries[0].sql, /generalId/); }); test('old sample paths fail with actionable guidance, explicit chosen target resolves them', async () => { const { run } = importer(), rows = [{ 学段: '小学', 教材: '示例教材', 单元: '第一单元', 单词: 'time' }]; const invalid = await run(rows); assert.equal(invalid.summary.error, 1); assert.match(invalid.items[0].errors[0], /示例教材.*单元ID.*页面选择/); const valid = await run(rows, 42); assert.equal(valid.summary.create, 1); assert.equal(valid.items[0].nodeId, 42); assert.equal(valid.summary.missingMedia, 1); }); test('explicit invalid, recycled or cross-tenant IDs never fall back to matching names', async () => { const { run } = importer(); const result = await run([{ 单元ID: '999', 学段: '小学', 教材: '真实教材', 单元: '第一单元', 单词: 'time' }]); assert.equal(result.summary.error, 1); assert.match(result.items[0].errors[0], /单元ID不存在/); await assert.rejects(run([{ 单词: 'time' }], 999), /有效的目标/); }); test('duplicate names require ID rather than choosing the last matching directory', async () => { const { run } = importer([42, 43].map(nodeId => ({ nodeId, nodeName: '第一单元', bookName: '真实教材', stageName: '小学' }))); assert.match((await run([{ 学段: '小学', 教材: '真实教材', 单元: '第一单元', 单词: 'time' }])).items[0].errors[0], /不唯一/); }); test('duplicate rows identify the original Excel row; different units remain separate', async () => { const { run } = importer([42, 43].map(nodeId => ({ nodeId }))); const result = await run([{ 单元ID: 42, 单词: 'time' }, { 单元ID: 42, 单词: 'TIME' }, { 单元ID: 43, 单词: 'time' }]); assert.equal(result.summary.duplicate, 1); assert.equal(result.summary.create, 2); assert.match(result.items[1].errors[0], /第 2 行/); }); test('unknown GeneralID blocks import rather than creating an unexpected word', async () => { const result = await importer().run([{ 'GeneralID(可选)': 999, 单元ID: 42, 单词: 'time' }]); assert.equal(result.summary.error, 1); assert.match(result.items[0].errors[0], /GeneralID.*不存在/); });