feat: notify tenants about public bank syncs

This commit is contained in:
Codex
2026-06-29 16:25:13 +08:00
parent e54efbc294
commit 1bcd887731
17 changed files with 558 additions and 24 deletions

View File

@@ -4238,6 +4238,46 @@ async function testPublicQuestionBankAdoption() {
'public bank sync should insert newly published source questions',
);
const syncNotifications = await request('/api/tenant-content/notifications', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
query: { notificationType: 'public_question_bank_synced', adoptionId: adopted.item.id },
});
const syncNotification = syncNotifications.items?.find(item => item.adoptionId === adopted.item.id && item.status === 'unread');
assert.ok(syncNotification?.id, 'public bank sync with inserted questions should create an unread tenant notification');
assert.equal(syncNotification?.severity, 'success', 'successful public bank sync notification should be success severity');
assert.equal(syncNotifications.summary?.unread >= 1, true, 'notification summary should include unread public bank sync notification');
assert.ok(!JSON.stringify(syncNotification).includes('新增公共题应同步到已采纳租户'), 'public bank notification should not leak question explanations');
const markedSyncNotification = await request('/api/tenant-content/notifications/status', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
method: 'POST',
body: { notificationIds: [syncNotification.id], status: 'read' },
});
assert.equal(markedSyncNotification.item?.updatedCount, 1, 'tenant admin should mark content notification as read');
const invalidNotificationId = await request('/api/tenant-content/notifications/status', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
method: 'POST',
body: { notificationIds: ['not-a-uuid'], status: 'read' },
expectStatus: 400,
});
assert.equal(invalidNotificationId.code, 'CONTENT_NOTIFICATION_ID_INVALID', 'notification status API should reject malformed UUIDs before querying');
const crossTenantNotifications = await request('/api/tenant-content/notifications', {
userId: TENANT_ADMIN_USER_ID,
query: { adoptionId: adopted.item.id },
});
assert.equal(crossTenantNotifications.items?.length, 0, 'content notifications must not expose partner tenant rows to another tenant admin');
const crossTenantNotificationsDenied = await request('/api/tenant-content/notifications', {
tenantId: PARTNER_TENANT_ID,
userId: TENANT_ADMIN_USER_ID,
query: { adoptionId: adopted.item.id },
expectStatus: 403,
});
assert.equal(crossTenantNotificationsDenied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'content notifications must require tenant content permission in the current tenant');
const collectionAfterSync = await request('/api/catalog/question-collections/questions', {
tenantId: PARTNER_TENANT_ID,
userId: false,
@@ -4302,6 +4342,16 @@ async function testPublicQuestionBankAdoption() {
'public bank sync should identify the source question that conflicts with tenant edits',
);
const conflictNotifications = await request('/api/tenant-content/notifications', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
query: { notificationType: 'public_question_bank_conflict', adoptionId: adopted.item.id },
});
const conflictNotification = conflictNotifications.items?.find(item => item.adoptionId === adopted.item.id && item.status === 'unread');
assert.ok(conflictNotification?.id, 'public bank conflicts should create an unread tenant notification');
assert.equal(conflictNotification?.severity, 'warning', 'public bank conflict notification should be warning severity');
assert.equal(conflictNotification?.metadata?.syncStatus, 'conflict', 'public bank conflict notification should expose conflict status metadata');
const conflictList = await request('/api/tenant-content/public-question-banks/conflicts', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
@@ -4369,6 +4419,15 @@ async function testPublicQuestionBankAdoption() {
query: { adoptionId: adopted.item.id },
});
assert.equal(conflictListAfterAccept.item?.conflictCount, 0, 'resolved conflict list should have no remaining conflicts');
const conflictNotificationsAfterAccept = await request('/api/tenant-content/notifications', {
tenantId: PARTNER_TENANT_ID,
userId: PARTNER_TENANT_ADMIN_USER_ID,
query: { notificationType: 'public_question_bank_conflict', adoptionId: adopted.item.id, status: 'resolved' },
});
assert.ok(
conflictNotificationsAfterAccept.items?.some(item => item.id === conflictNotification.id && item.status === 'resolved'),
'resolving all public bank conflicts should mark related notifications resolved',
);
const keepLocalContent = `租户再次保留本地公共题副本 ${Date.now()}`;
await request('/api/tenant-content/questions', {