forked from wangziqi/gongxue-base
feat: add vocabulary handbook import pipeline
This commit is contained in:
@@ -802,6 +802,231 @@ async function testTenantContentAssetsAndImports() {
|
||||
'catalog should expose imported question through the new collection binding',
|
||||
);
|
||||
|
||||
const vocabEntry = await request('/api/tenant-content/content-entries', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryKey: 'integration-vocabulary',
|
||||
regionId: ids.region,
|
||||
name: '集成测试背单词入口',
|
||||
entryType: 'vocabulary',
|
||||
route: '/vocabulary',
|
||||
order: 21,
|
||||
},
|
||||
});
|
||||
const vocabRoot = await request('/api/tenant-content/content-nodes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: vocabEntry.item.id,
|
||||
regionId: ids.region,
|
||||
nodeKey: 'integration-vocabulary-root',
|
||||
name: '英语核心词',
|
||||
nodeType: 'category',
|
||||
isLeaf: false,
|
||||
},
|
||||
});
|
||||
|
||||
const deniedVocabularyPreview = await request('/api/tenant-content/imports/preview/vocabulary', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
units: [{ name: '学生不能导入单词', words: [{ word: 'deny', meaning: '拒绝' }] }],
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(deniedVocabularyPreview.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not preview vocabulary import');
|
||||
|
||||
const invalidVocabularyPreview = await request('/api/tenant-content/imports/preview/vocabulary', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'invalid-vocabulary-import.json',
|
||||
entryId: vocabEntry.item.id,
|
||||
contentNodeId: vocabRoot.item.id,
|
||||
vocabulary_units_示例数据: [
|
||||
{ legacyId: 'integration-vocab-invalid-unit', name: '旧格式错误单词单元', order: 1 },
|
||||
],
|
||||
vocabulary_示例数据: [
|
||||
{ unitId: 'integration-vocab-invalid-unit', word: '', meaning: '', order: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.ok(invalidVocabularyPreview.issues?.some(issue => issue.code === 'VOCABULARY_WORD_REQUIRED'), 'invalid vocabulary preview should validate word');
|
||||
assert.ok(invalidVocabularyPreview.issues?.some(issue => issue.code === 'VOCABULARY_MEANING_REQUIRED'), 'invalid vocabulary preview should validate meaning');
|
||||
|
||||
const rejectedVocabularyImport = await request('/api/tenant-content/imports/vocabulary', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: invalidVocabularyPreview.job.id },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(rejectedVocabularyImport.code, 'IMPORT_HAS_ERRORS', 'invalid vocabulary import should be rejected');
|
||||
|
||||
const vocabularyPreview = await request('/api/tenant-content/imports/preview/vocabulary', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'legacy-vocabulary-import.json',
|
||||
regionId: ids.region,
|
||||
entryId: vocabEntry.item.id,
|
||||
contentNodeId: vocabRoot.item.id,
|
||||
vocabulary_units_示例数据: [
|
||||
{
|
||||
legacyId: 'integration-vocab-unit-001',
|
||||
name: 'Unit 1 - 高频核心词',
|
||||
description: '旧模板字段导入验证',
|
||||
order: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
vocabulary_示例数据: [
|
||||
{
|
||||
legacyId: 'integration-vocab-word-abandon',
|
||||
unitId: 'integration-vocab-unit-001',
|
||||
word: 'abandon',
|
||||
phonetic: '/əˈbændən/',
|
||||
meaning: 'v. 放弃,抛弃',
|
||||
example: 'He had to abandon his car in the snow.',
|
||||
exampleTranslation: '他不得不把车丢弃在雪地里。',
|
||||
difficulty: 3,
|
||||
tags: ['高频词', '考纲核心'],
|
||||
order: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(vocabularyPreview.job?.errorCount, 0, 'valid vocabulary preview should have no errors');
|
||||
|
||||
const vocabularyImport = await request('/api/tenant-content/imports/vocabulary', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: vocabularyPreview.job.id },
|
||||
});
|
||||
assert.equal(vocabularyImport.item?.status, 'completed', 'valid vocabulary import should complete');
|
||||
assert.ok(
|
||||
(vocabularyImport.item?.insertedCount || 0) + (vocabularyImport.item?.updatedCount || 0) + (vocabularyImport.item?.skippedCount || 0) >= 2,
|
||||
'vocabulary import should process unit and word idempotently',
|
||||
);
|
||||
|
||||
const vocabularyUnits = await request('/api/catalog/vocabulary-units', {
|
||||
query: { regionId: ids.region },
|
||||
});
|
||||
const importedVocabularyUnit = vocabularyUnits.items?.find(item => item.legacyId === 'integration-vocab-unit-001');
|
||||
assert.ok(importedVocabularyUnit, 'catalog should expose imported vocabulary unit');
|
||||
assert.equal(importedVocabularyUnit.entryId, vocabEntry.item.id, 'vocabulary unit should bind content entry');
|
||||
|
||||
const vocabularyWords = await request('/api/catalog/vocabulary-words', {
|
||||
query: { unitId: importedVocabularyUnit.id },
|
||||
});
|
||||
assert.ok(vocabularyWords.items?.some(item => item.word === 'abandon' && item.contentNodeId), 'catalog should expose imported vocabulary word with node binding');
|
||||
|
||||
const handbookEntry = await request('/api/tenant-content/content-entries', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryKey: 'integration-handbook',
|
||||
regionId: ids.region,
|
||||
name: '集成测试知识手册入口',
|
||||
entryType: 'handbook',
|
||||
route: '/handbook',
|
||||
order: 22,
|
||||
},
|
||||
});
|
||||
const handbookRoot = await request('/api/tenant-content/content-nodes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: handbookEntry.item.id,
|
||||
regionId: ids.region,
|
||||
nodeKey: 'integration-handbook-root',
|
||||
name: '文化课',
|
||||
nodeType: 'category',
|
||||
isLeaf: false,
|
||||
},
|
||||
});
|
||||
|
||||
const handbookPreview = await request('/api/tenant-content/imports/preview/handbook', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'handbook-nested-import.json',
|
||||
regionId: ids.region,
|
||||
entryId: handbookEntry.item.id,
|
||||
contentNodeId: handbookRoot.item.id,
|
||||
subjects: [
|
||||
{
|
||||
legacyId: 'integration-handbook-chinese',
|
||||
name: '大学语文',
|
||||
type: 'guide',
|
||||
icon: 'book-open',
|
||||
color: '#10B981',
|
||||
chapters: [
|
||||
{
|
||||
legacyId: 'integration-handbook-chapter-outline',
|
||||
name: '一、语文考纲',
|
||||
sections: [
|
||||
{
|
||||
legacyId: 'integration-handbook-section-outline',
|
||||
name: '考纲解读',
|
||||
entries: [
|
||||
{
|
||||
legacyId: 'integration-handbook-entry-outline',
|
||||
title: '2024年天津专升本语文考试大纲',
|
||||
summary: '全面解读语文考试要求和考点分布',
|
||||
content: '一、考试性质\n\n天津市高职升本科招生统一考试是选拔性考试。',
|
||||
tags: ['考纲', '必读'],
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(handbookPreview.job?.errorCount, 0, 'valid handbook preview should have no errors');
|
||||
|
||||
const handbookImport = await request('/api/tenant-content/imports/handbook', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: handbookPreview.job.id },
|
||||
});
|
||||
assert.equal(handbookImport.item?.status, 'completed', 'valid handbook import should complete');
|
||||
assert.ok(
|
||||
(handbookImport.item?.insertedCount || 0) + (handbookImport.item?.updatedCount || 0) + (handbookImport.item?.skippedCount || 0) >= 3,
|
||||
'handbook import should process subject, chapter, and entry idempotently',
|
||||
);
|
||||
|
||||
const handbookSubjects = await request('/api/catalog/handbook-subjects', {
|
||||
query: { regionId: ids.region },
|
||||
});
|
||||
const importedHandbookSubject = handbookSubjects.items?.find(item => item.legacyId === 'integration-handbook-chinese');
|
||||
assert.ok(importedHandbookSubject, 'catalog should expose imported handbook subject');
|
||||
assert.equal(importedHandbookSubject.entryId, handbookEntry.item.id, 'handbook subject should bind content entry');
|
||||
|
||||
const handbookChapters = await request('/api/catalog/handbook-chapters', {
|
||||
query: { subjectId: importedHandbookSubject.id },
|
||||
});
|
||||
const importedHandbookChapter = handbookChapters.items?.find(item => item.legacyId === 'integration-handbook-chapter-outline');
|
||||
assert.ok(importedHandbookChapter, 'catalog should expose imported handbook chapter');
|
||||
|
||||
const handbookEntries = await request('/api/catalog/handbook-entries', {
|
||||
query: { chapterId: importedHandbookChapter.id, includeContent: true },
|
||||
});
|
||||
assert.ok(
|
||||
handbookEntries.items?.some(item => item.legacyId === 'integration-handbook-entry-outline' && item.content?.includes('选拔性考试')),
|
||||
'catalog should expose imported handbook entry content',
|
||||
);
|
||||
|
||||
const importJobs = await request('/api/tenant-content/imports', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { importType: 'vocabulary', limit: 10 },
|
||||
});
|
||||
assert.ok(importJobs.items?.some(item => item.id === vocabularyPreview.job.id && item.status === 'completed'), 'import job list should include completed vocabulary job');
|
||||
|
||||
const partnerImports = await request('/api/tenant-content/imports', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
|
||||
Reference in New Issue
Block a user