fix(reports): separate readable categories from report content

Store readable category snapshots separately from details and evidence, validate report categories, and preserve context in admin displays and moderation actions. Widen report categories to TEXT and bump the schema to 3.2.0 without legacy data conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-31 00:57:13 +08:00
1 parent b2811cc248
commit b45fd161f0
16 files changed
+797 -75

No files matched your search

@@ -65,8 +65,8 @@ class MiniFormData {
}
function createRuntime({ imgEnabled = true, categories = [
{ slug: 'spam', name: '垃圾信息' },
{ slug: 'abuse', name: '人身攻击' },
{ name: '垃圾信息' },
{ name: '人身攻击' },
] } = {}) {
const document = new ReportDocument();
const modals = [];
@@ -192,7 +192,7 @@ async function flushPromises() {
}
test('opens from an article button, associates the target, omits URL input, and escapes rendered names', () => {
const runtime = createRuntime({ categories: [{ slug: 'unsafe', name: '<svg onload="run()">reason</svg>' }] });
const runtime = createRuntime({ categories: [{ name: '<svg onload="run()">reason</svg>' }] });
const targetName = '<img src=x onerror="run()"> &';
const modal = runtime.openReport({ target_type: 'post', target_id: 73, target_name: targetName });
const target = modal.shell.querySelector('.aksrt-report-target__name');
@@ -249,8 +249,8 @@ test('requires a radio reason and other text, then sends canonical fields with d
assert.deepEqual(payload, {
target_type: 'post',
target_id: 42,
category: '',
reason: '自定义原因\n详细描述:\n补充背景',
category: '其他原因',
reason: '自定义原因\n\n补充背景',
});
call.resolve({ success: true, message: '已提交' });
await flushPromises();
@@ -273,8 +273,34 @@ test('preset reason ignores hidden custom text while including description in re
assert.deepEqual(payload, {
target_type: 'post',
target_id: 42,
category: 'abuse',
reason: '人身攻击\n详细描述:\nMore context',
category: '人身攻击',
reason: 'More context',
});
});
test('allows a preset-only report without copying the category into content', async () => {
const runtime = createRuntime();
const modal = runtime.openReport();
selectReason(modal, 0);
submit(modal);
const request = callsFor(runtime, '/report/submit')[0];
assert.deepEqual(payloadFor(request), {
target_type: 'post', target_id: 42, category: '垃圾信息', reason: '',
});
request.resolve({ success: true });
await flushPromises();
});
test('keeps a single other option and preserves explicitly entered category words in content', () => {
const runtime = createRuntime({ categories: [{ name: '其他原因' }, { name: '垃圾信息' }] });
const modal = runtime.openReport();
const radios = modal.shell.querySelectorAll('[name="report_reason"]');
assert.equal(radios.length, 2);
selectReason(modal, 0);
modal.shell.querySelector('[name="report_desc"]').value = '垃圾信息中包含了违规内容';
submit(modal);
assert.deepEqual(payloadFor(callsFor(runtime, '/report/submit')[0]), {
target_type: 'post', target_id: 42, category: '垃圾信息', reason: '垃圾信息中包含了违规内容',
});
});
@@ -381,7 +407,7 @@ test('server failure preserves values, reenables controls, and allows a retry',
submit(modal);
const retry = callsFor(runtime, '/report/submit')[1];
assert.equal(payloadFor(retry).reason, 'A custom report reason\n详细描述:\nAdditional context');
assert.equal(payloadFor(retry).reason, 'A custom report reason\n\nAdditional context');
retry.resolve({ success: true });
await flushPromises();
});