feat: add community publishing and news syndication
This commit is contained in:
@@ -1,136 +1,206 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
assessCommunityModeration,
|
||||
findLocalModerationMatches,
|
||||
normalizeModerationTerm,
|
||||
parseModerationTermBatch,
|
||||
type CommunityModerationPolicy
|
||||
assessCommunityModeration,
|
||||
findLocalModerationMatches,
|
||||
normalizeModerationTerm,
|
||||
parseModerationTermBatch,
|
||||
type CommunityModerationPolicy,
|
||||
} from './community-moderation';
|
||||
|
||||
const policy = (overrides: Partial<CommunityModerationPolicy> = {}): CommunityModerationPolicy => ({
|
||||
automatedEnabled: true,
|
||||
autoPublishClean: true,
|
||||
holdOnProviderError: true,
|
||||
scanComments: true,
|
||||
scanProfiles: true,
|
||||
scanGuides: true,
|
||||
scanAvatars: true,
|
||||
providerModel: 'omni-moderation-latest',
|
||||
detectPii: true,
|
||||
maxLinks: 1,
|
||||
duplicateWindowSeconds: 300,
|
||||
policyVersion: 1,
|
||||
thresholds: [{
|
||||
category: 'harassment',
|
||||
enabled: true,
|
||||
reviewThreshold: 0.5,
|
||||
priorityThreshold: 0.85
|
||||
}],
|
||||
terms: [],
|
||||
...overrides
|
||||
const policy = (
|
||||
overrides: Partial<CommunityModerationPolicy> = {},
|
||||
): CommunityModerationPolicy => ({
|
||||
automatedEnabled: true,
|
||||
autoPublishClean: true,
|
||||
holdOnProviderError: true,
|
||||
scanComments: true,
|
||||
scanProfiles: true,
|
||||
scanGuides: true,
|
||||
scanAvatars: true,
|
||||
providerModel: 'omni-moderation-latest',
|
||||
detectPii: true,
|
||||
maxLinks: 1,
|
||||
duplicateWindowSeconds: 300,
|
||||
policyVersion: 1,
|
||||
thresholds: [
|
||||
{
|
||||
category: 'harassment',
|
||||
enabled: true,
|
||||
reviewThreshold: 0.5,
|
||||
priorityThreshold: 0.85,
|
||||
},
|
||||
],
|
||||
terms: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('community moderation policy', () => {
|
||||
it('normalizes configured terms consistently', () => {
|
||||
expect(normalizeModerationTerm(' Rogue\u00a0Agent ')).toBe('rogue agent');
|
||||
});
|
||||
it('holds provider-required Guide content when classification fails', () => {
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy({ automatedEnabled: false, holdOnProviderError: false }),
|
||||
contentType: 'guide',
|
||||
providerFlagged: false,
|
||||
categoryScores: {},
|
||||
ruleMatches: [],
|
||||
providerError: 'unavailable',
|
||||
providerRequired: true,
|
||||
}).decision,
|
||||
).toBe('error');
|
||||
});
|
||||
|
||||
it('parses pasted terms with automatic word and phrase matching', () => {
|
||||
expect(parseModerationTermBatch('weapon\nkill, machine gun; assault rifle')).toEqual({
|
||||
terms: [
|
||||
{ value: 'weapon', normalizedValue: 'weapon', matchMode: 'word' },
|
||||
{ value: 'kill', normalizedValue: 'kill', matchMode: 'word' },
|
||||
{ value: 'machine gun', normalizedValue: 'machine gun', matchMode: 'phrase' },
|
||||
{ value: 'assault rifle', normalizedValue: 'assault rifle', matchMode: 'phrase' }
|
||||
],
|
||||
duplicateCount: 0
|
||||
});
|
||||
});
|
||||
it('normalizes configured terms consistently', () => {
|
||||
expect(normalizeModerationTerm(' Rogue\u00a0Agent ')).toBe('rogue agent');
|
||||
});
|
||||
|
||||
it('deduplicates pasted terms after compatibility and case normalization', () => {
|
||||
const parsed = parseModerationTermBatch('Weapon\nweapon\nMACHINE GUN\nmachine gun');
|
||||
expect(parsed.terms).toEqual([
|
||||
{ value: 'Weapon', normalizedValue: 'weapon', matchMode: 'word' },
|
||||
{ value: 'MACHINE GUN', normalizedValue: 'machine gun', matchMode: 'phrase' }
|
||||
]);
|
||||
expect(parsed.duplicateCount).toBe(2);
|
||||
});
|
||||
it('parses pasted terms with automatic word and phrase matching', () => {
|
||||
expect(
|
||||
parseModerationTermBatch('weapon\nkill, machine gun; assault rifle'),
|
||||
).toEqual({
|
||||
terms: [
|
||||
{ value: 'weapon', normalizedValue: 'weapon', matchMode: 'word' },
|
||||
{ value: 'kill', normalizedValue: 'kill', matchMode: 'word' },
|
||||
{
|
||||
value: 'machine gun',
|
||||
normalizedValue: 'machine gun',
|
||||
matchMode: 'phrase',
|
||||
},
|
||||
{
|
||||
value: 'assault rifle',
|
||||
normalizedValue: 'assault rifle',
|
||||
matchMode: 'phrase',
|
||||
},
|
||||
],
|
||||
duplicateCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows an exact local exception to suppress a matching banned term', () => {
|
||||
const matches = findLocalModerationMatches('Rogue Agent tactics', 'comment', policy({
|
||||
detectPii: false,
|
||||
terms: [
|
||||
{ id: 'ban', listType: 'ban', matchMode: 'phrase', value: 'Rogue Agent', normalizedValue: 'rogue agent', scopes: ['comment'], enabled: true },
|
||||
{ id: 'allow', listType: 'allow', matchMode: 'phrase', value: 'Rogue Agent', normalizedValue: 'rogue agent', scopes: ['comment'], enabled: true }
|
||||
]
|
||||
}));
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
it('deduplicates pasted terms after compatibility and case normalization', () => {
|
||||
const parsed = parseModerationTermBatch(
|
||||
'Weapon\nweapon\nMACHINE GUN\nmachine gun',
|
||||
);
|
||||
expect(parsed.terms).toEqual([
|
||||
{ value: 'Weapon', normalizedValue: 'weapon', matchMode: 'word' },
|
||||
{
|
||||
value: 'MACHINE GUN',
|
||||
normalizedValue: 'machine gun',
|
||||
matchMode: 'phrase',
|
||||
},
|
||||
]);
|
||||
expect(parsed.duplicateCount).toBe(2);
|
||||
});
|
||||
|
||||
it('finds scoped banned terms, PII, and excessive links', () => {
|
||||
const matches = findLocalModerationMatches(
|
||||
'Contact test@example.com about forbidden plans at https://one.test and https://two.test',
|
||||
'comment',
|
||||
policy({
|
||||
terms: [{
|
||||
id: 'ban',
|
||||
listType: 'ban',
|
||||
matchMode: 'word',
|
||||
value: 'forbidden',
|
||||
normalizedValue: 'forbidden',
|
||||
scopes: ['comment'],
|
||||
enabled: true
|
||||
}]
|
||||
})
|
||||
);
|
||||
expect(matches.map((match) => match.kind)).toEqual(['banned_term', 'pii', 'link_limit']);
|
||||
});
|
||||
it('allows an exact local exception to suppress a matching banned term', () => {
|
||||
const matches = findLocalModerationMatches(
|
||||
'Rogue Agent tactics',
|
||||
'comment',
|
||||
policy({
|
||||
detectPii: false,
|
||||
terms: [
|
||||
{
|
||||
id: 'ban',
|
||||
listType: 'ban',
|
||||
matchMode: 'phrase',
|
||||
value: 'Rogue Agent',
|
||||
normalizedValue: 'rogue agent',
|
||||
scopes: ['comment'],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'allow',
|
||||
listType: 'allow',
|
||||
matchMode: 'phrase',
|
||||
value: 'Rogue Agent',
|
||||
normalizedValue: 'rogue agent',
|
||||
scopes: ['comment'],
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it('routes priority scores ahead of ordinary review scores', () => {
|
||||
expect(assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'comment',
|
||||
providerFlagged: true,
|
||||
categoryScores: { harassment: 0.9 },
|
||||
ruleMatches: []
|
||||
}).decision).toBe('priority_review');
|
||||
});
|
||||
it('finds scoped banned terms, PII, and excessive links', () => {
|
||||
const matches = findLocalModerationMatches(
|
||||
'Contact test@example.com about forbidden plans at https://one.test and https://two.test',
|
||||
'comment',
|
||||
policy({
|
||||
terms: [
|
||||
{
|
||||
id: 'ban',
|
||||
listType: 'ban',
|
||||
matchMode: 'word',
|
||||
value: 'forbidden',
|
||||
normalizedValue: 'forbidden',
|
||||
scopes: ['comment'],
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(matches.map((match) => match.kind)).toEqual([
|
||||
'banned_term',
|
||||
'pii',
|
||||
'link_limit',
|
||||
]);
|
||||
});
|
||||
|
||||
it('holds provider failures under the default fail-closed policy', () => {
|
||||
expect(assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'profile_bio',
|
||||
providerFlagged: false,
|
||||
categoryScores: {},
|
||||
ruleMatches: [],
|
||||
providerError: 'timeout'
|
||||
}).decision).toBe('error');
|
||||
});
|
||||
it('routes priority scores ahead of ordinary review scores', () => {
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'comment',
|
||||
providerFlagged: true,
|
||||
categoryScores: { harassment: 0.9 },
|
||||
ruleMatches: [],
|
||||
}).decision,
|
||||
).toBe('priority_review');
|
||||
});
|
||||
|
||||
it('publishes clean content only when auto publication is enabled', () => {
|
||||
expect(assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: { harassment: 0.01 },
|
||||
ruleMatches: []
|
||||
}).decision).toBe('approved');
|
||||
expect(assessCommunityModeration({
|
||||
policy: policy({ autoPublishClean: false }),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: { harassment: 0.01 },
|
||||
ruleMatches: []
|
||||
}).decision).toBe('review');
|
||||
});
|
||||
it('holds provider failures under the default fail-closed policy', () => {
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'profile_bio',
|
||||
providerFlagged: false,
|
||||
categoryScores: {},
|
||||
ruleMatches: [],
|
||||
providerError: 'timeout',
|
||||
}).decision,
|
||||
).toBe('error');
|
||||
});
|
||||
|
||||
it('publishes locally clean content when provider scanning is disabled', () => {
|
||||
expect(assessCommunityModeration({
|
||||
policy: policy({ automatedEnabled: false }),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: {},
|
||||
ruleMatches: []
|
||||
}).decision).toBe('approved');
|
||||
});
|
||||
it('publishes clean content only when auto publication is enabled', () => {
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy(),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: { harassment: 0.01 },
|
||||
ruleMatches: [],
|
||||
}).decision,
|
||||
).toBe('approved');
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy({ autoPublishClean: false }),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: { harassment: 0.01 },
|
||||
ruleMatches: [],
|
||||
}).decision,
|
||||
).toBe('review');
|
||||
});
|
||||
|
||||
it('publishes locally clean content when provider scanning is disabled', () => {
|
||||
expect(
|
||||
assessCommunityModeration({
|
||||
policy: policy({ automatedEnabled: false }),
|
||||
contentType: 'comment',
|
||||
providerFlagged: false,
|
||||
categoryScores: {},
|
||||
ruleMatches: [],
|
||||
}).decision,
|
||||
).toBe('approved');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user