Link prior moderation actions to case decisions
This commit is contained in:
@@ -216,7 +216,8 @@ it('preserves original evidence context, linked restrictions, and staff history
|
||||
'Restrict player',
|
||||
'Revoke restriction',
|
||||
'Add case note',
|
||||
'Attach evidence'
|
||||
'Attach evidence',
|
||||
'Link existing action'
|
||||
])
|
||||
expect(button(label)).toBeUndefined();
|
||||
expect(document.querySelector('dialog')).toBeNull();
|
||||
@@ -380,6 +381,122 @@ it('attaches archived evidence by match and exact message with revision checks',
|
||||
});
|
||||
});
|
||||
|
||||
it('links a prior mute with its recorded outcome without issuing another restriction', async () => {
|
||||
const initial = detail();
|
||||
const prior = {
|
||||
id: 'earlier-mute',
|
||||
type: 'restriction' as const,
|
||||
kind: 'mute',
|
||||
reason: 'Already applied before the report',
|
||||
actor: 'Moderator Two',
|
||||
createdAt: '2026-10-14T10:00:00Z',
|
||||
state: 'expired',
|
||||
expiresAt: '2026-10-14T11:00:00Z'
|
||||
};
|
||||
initial.availableActions = [prior];
|
||||
initial.linkedActions = [];
|
||||
const updated = detail();
|
||||
updated.linkedActions = [prior];
|
||||
updated.availableActions = [];
|
||||
updated.case.revision = 5;
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(response({ ok: true, id: 'case-1' }))
|
||||
.mockResolvedValue(response(updated));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show(initial);
|
||||
button('Link existing action').click();
|
||||
await tick();
|
||||
select('Existing action', 'restriction:earlier-mute');
|
||||
expect(document.querySelector('dialog[open]')?.textContent).toContain('Expired');
|
||||
expect(document.querySelector('dialog[open]')?.textContent).toContain('Moderator Two');
|
||||
enter(field('Link reason'), 'This mute addressed the reported chat');
|
||||
button('Link action').click();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toEqual({
|
||||
op: 'link-case-action',
|
||||
id: 'case-1',
|
||||
revision: 4,
|
||||
actionType: 'restriction',
|
||||
actionId: 'earlier-mute',
|
||||
reason: 'This mute addressed the reported chat',
|
||||
idempotencyKey: expect.any(String)
|
||||
});
|
||||
expect(document.querySelector('[aria-labelledby="actions-title"]')?.textContent).toContain(
|
||||
'Already applied before the report'
|
||||
);
|
||||
expect(document.querySelector('[aria-labelledby="actions-title"]')?.textContent).toContain(
|
||||
'Expired'
|
||||
);
|
||||
expect(button('Link existing action').disabled).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['warning_issued', 'action:warning-1', 'action', 'warning-1'],
|
||||
['restriction_applied', 'restriction:restriction-1', 'restriction', 'restriction-1']
|
||||
] as const)(
|
||||
'references a linked action when recording %s',
|
||||
async (outcome, selection, type, id) => {
|
||||
const initial = detail();
|
||||
initial.linkedActions = [
|
||||
{
|
||||
id: 'warning-1',
|
||||
type: 'action',
|
||||
kind: 'warn',
|
||||
reason: 'Confirmed warning',
|
||||
actor: 'Moderator One',
|
||||
createdAt: '2026-10-15T10:00:00Z',
|
||||
state: 'confirmed',
|
||||
expiresAt: null
|
||||
},
|
||||
{
|
||||
id: 'failed-warning',
|
||||
type: 'action',
|
||||
kind: 'warn',
|
||||
reason: 'Undelivered warning',
|
||||
actor: 'Moderator One',
|
||||
createdAt: '2026-10-15T09:00:00Z',
|
||||
state: 'failed',
|
||||
expiresAt: null
|
||||
},
|
||||
{
|
||||
id: 'restriction-1',
|
||||
type: 'restriction',
|
||||
kind: 'mute',
|
||||
reason: 'Prior mute',
|
||||
actor: 'Moderator One',
|
||||
createdAt: '2026-10-15T11:00:00Z',
|
||||
state: 'expired',
|
||||
expiresAt: '2026-10-15T12:00:00Z'
|
||||
}
|
||||
];
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(response({ ok: true, id: 'case-1' }))
|
||||
.mockResolvedValue(response(initial));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show(initial);
|
||||
button('Review case').click();
|
||||
await tick();
|
||||
select('Outcome', outcome);
|
||||
const options = [...field<HTMLSelectElement>('Supporting action').options].map(
|
||||
(option) => option.value
|
||||
);
|
||||
expect(options).toEqual(['', selection]);
|
||||
select('Supporting action', selection);
|
||||
enter(field('Review reason'), 'Reviewed the recorded action');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({
|
||||
op: 'resolve-case',
|
||||
id: 'case-1',
|
||||
revision: 4,
|
||||
status: outcome,
|
||||
actionReference: { type, id }
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('retains a conflicted review and lets staff refresh before making another decision', async () => {
|
||||
const updated = detail();
|
||||
updated.case.revision = 8;
|
||||
|
||||
Reference in New Issue
Block a user