Files
shd.build/src/routes/isac/data-moderation/data-moderation-actions.test.ts
T

1104 lines
34 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { connect, type Database } from '@tursodatabase/database';
import { readFile } from 'node:fs/promises';
const mocks = vi.hoisted(() => ({
database: null as Database | null,
user: { id: 'reviewer' },
createNotification: vi.fn(),
}));
vi.mock('$lib/server/auth/session', () => ({
getSessionUser: vi.fn(async () => mocks.user),
}));
vi.mock('$lib/server/auth/authorization', () => ({
requirePermission: vi.fn(async (user) => user),
}));
vi.mock('$lib/server/database', () => ({
getDatabase: vi.fn(async () => mocks.database),
}));
vi.mock('$lib/server/notifications', () => ({
createNotification: mocks.createNotification,
}));
import { actions, load } from './+page.server';
let database: Database;
const applySchema = async (target: Database) => {
const schema = await readFile(
new URL('../../../../database/schema.sql', import.meta.url),
'utf8',
);
for (const statement of schema
.split(';')
.map((value) => value.trim())
.filter(Boolean)) {
await target.exec(statement);
}
};
const payload = (fields: Record<string, string>) =>
JSON.stringify(
Object.entries(fields).map(([name, value]) => ({
name,
kind: 'text',
value,
})),
);
const insertProposal = async (
id: string,
fields: Record<string, string>,
options: {
draftEntityId?: string;
entityId?: string;
module?: string;
action?: string;
status?: string;
} = {},
) => {
await (
await database.prepare(`INSERT INTO data_change_proposals
(id,module,action,entity_id,draft_entity_id,summary,payload_json,status,proposed_by,created_at)
VALUES(?,?,?,?,?,?,?,?,'contributor','now')`)
).run(
id,
options.module ?? 'attributes',
options.action ?? 'save',
options.entityId ?? null,
options.draftEntityId ?? null,
`SAVE · ${fields.name ?? id}`,
payload(fields),
options.status ?? 'pending',
);
};
const protocolSource = () => ({
version: 1,
template: 'Gain {1}% weapon damage.',
variants: [
{
key: 'base',
materialized: 'Gain 10% weapon damage.',
slots: [{ key: '1', value: '10', order: 0 }],
},
],
});
const insertProtocolProfile = async () => {
await database.exec(`
INSERT INTO os_protocol_secondary_profiles(
id,quality,primary_attribute_type,created_at,updated_at
) VALUES('profile-route','Standard','Firepower','now','now');
INSERT INTO os_protocol_secondary_effects(
id,profile_id,effect_role,stat_attribute_id,value
) VALUES
('route-core','profile-route','Core','firepower',1),
('route-damage','profile-route','Damage','weapon-damage-profile',2),
('route-health','profile-route','Health','health',3);
`);
};
const insertSpecialization = async () => {
await database.exec(`
INSERT INTO specializations(id,name,created_at,updated_at)
VALUES('specialization-route','Route Specialization','now','now');
`);
};
const actionRequest = (id: string, fields: Record<string, string>) => {
const form = new FormData();
form.set('id', id);
for (const [name, value] of Object.entries(fields)) form.set(name, value);
return new Request('http://localhost/isac/data-moderation', {
method: 'POST',
body: form,
});
};
beforeEach(async () => {
database = await connect(':memory:');
mocks.database = database;
mocks.createNotification.mockReset();
await database.exec('PRAGMA foreign_keys=ON');
await applySchema(database);
await database.exec(`
INSERT INTO users(id,username,role,created_at)
VALUES('reviewer','Reviewer','admin','now'),('contributor','Contributor','admin','now');
INSERT INTO stat_attributes(id,name,type,category,calculation_type,unit,created_at,updated_at)
VALUES
('firepower','Firepower','Offensive','Character','Additive','','now','now'),
('weapon-damage-profile','Weapon Damage','Offensive','Weapon','Additive','','now','now'),
('health','Health','Defensive','Character','Additive','','now','now');
`);
});
afterEach(async () => {
await database.close();
mocks.database = null;
});
describe('Data Oversight queue loading', () => {
it('marks malformed stored payloads unavailable without mutating them', async () => {
await insertProposal('valid-payload', { name: 'Valid payload' });
await insertProposal('malformed-payload', { name: 'Malformed payload' });
await (
await database.prepare(
'UPDATE data_change_proposals SET payload_json=? WHERE id=?',
)
).run('not-json', 'malformed-payload');
const result = (await load({
cookies: {} as never,
url: new URL('http://localhost/isac/data-moderation'),
} as never)) as { proposals: Array<{ id: string; payloadValid: boolean }> };
expect(
result.proposals.find((proposal) => proposal.id === 'valid-payload'),
).toMatchObject({ payloadValid: true });
expect(
result.proposals.find((proposal) => proposal.id === 'malformed-payload'),
).toMatchObject({ payloadValid: false });
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('malformed-payload'),
).toEqual({ payload_json: 'not-json', status: 'pending' });
});
});
describe('Data Oversight approval', () => {
it('marks malformed stored Phase 2 payloads failed without exposing parser details', async () => {
await (
await database.prepare(`INSERT INTO data_change_proposals
(id,module,action,draft_entity_id,summary,payload_json,status,proposed_by,created_at)
VALUES(?,?,?,?,?,'not-json','pending','contributor','now')`)
).run(
'malformed-phase2-payload',
'protocols',
'save',
'malformed-protocol-draft',
'SAVE · Malformed payload',
);
const result = await actions.approve?.({
cookies: {} as never,
request: actionRequest('malformed-phase2-payload', {
note: 'approval note',
}),
fetch: vi.fn(),
url: new URL('http://localhost/isac/data-moderation'),
} as never);
expect(result).toMatchObject({ status: 409 });
expect(
await (
await database.prepare(
'SELECT status,applied_entity_id FROM data_change_proposals WHERE id=?',
)
).get('malformed-phase2-payload'),
).toEqual({ status: 'failed', applied_entity_id: null });
const audit = await (
await database.prepare(
"SELECT after_json FROM audit_log WHERE action='proposal.failed' AND entity_id='malformed-phase2-payload'",
)
).get();
expect(JSON.parse(String(audit?.after_json))).toEqual({
status: 'failed',
messagePresent: true,
});
expect(String(audit?.after_json)).not.toContain('SyntaxError');
expect(
await (
await database.prepare('SELECT COUNT(*) AS count FROM os_protocols')
).get(),
).toEqual({ count: 0 });
});
it('marks malformed non-Phase-2 payloads failed with a controlled response', async () => {
await insertProposal('malformed-non-phase2-payload', {
name: 'Malformed payload',
});
await (
await database.prepare(
'UPDATE data_change_proposals SET payload_json=? WHERE id=?',
)
).run('not-json', 'malformed-non-phase2-payload');
const result = await actions.approve?.({
cookies: {} as never,
request: actionRequest('malformed-non-phase2-payload', {
note: 'operator note',
}),
fetch: vi.fn(),
url: new URL('http://localhost/isac/data-moderation'),
} as never);
expect(result).toMatchObject({
status: 409,
data: {
moderationError:
'The proposal could not be deployed because its stored payload is invalid and was marked failed.',
},
});
expect(
await (
await database.prepare(
'SELECT status,reviewed_by,review_note FROM data_change_proposals WHERE id=?',
)
).get('malformed-non-phase2-payload'),
).toEqual({
status: 'failed',
reviewed_by: 'reviewer',
review_note: 'The stored proposal payload is invalid.',
});
const audit = await (
await database.prepare(
"SELECT before_json,after_json FROM audit_log WHERE action='proposal.failed' AND entity_id='malformed-non-phase2-payload'",
)
).get();
expect(JSON.stringify(audit)).not.toContain('not-json');
expect(JSON.parse(String(audit?.after_json))).toEqual({
status: 'failed',
reason: 'invalid_payload',
});
expect(mocks.createNotification).toHaveBeenCalledOnce();
expect(JSON.stringify(mocks.createNotification.mock.calls[0])).not.toContain(
'not-json',
);
});
it('stores only the canonical message when a non-Phase-2 action returns HTTP failure', async () => {
await insertProposal('canonical-http-failure', {
name: 'Canonical failure',
});
const sentinel = 'raw-response-body-sentinel-'.repeat(100);
const response = {
ok: false,
status: 422,
text: vi.fn(async () => sentinel),
} as unknown as Response;
const fetch = vi.fn(async () => response);
const result = await actions.approve?.({
cookies: {} as never,
request: actionRequest('canonical-http-failure', {
note: 'operator note',
}),
fetch,
url: new URL('http://localhost/isac/data-moderation'),
} as never);
expect(result).toMatchObject({
status: 409,
data: {
moderationError:
'The proposal did not pass the current canonical validation and was marked failed.',
},
});
expect(response.text).not.toHaveBeenCalled();
const proposal = await (
await database.prepare(
'SELECT status,review_note FROM data_change_proposals WHERE id=?',
)
).get('canonical-http-failure');
expect(proposal).toEqual({
status: 'failed',
review_note: 'The proposal did not pass current canonical validation.',
});
const audit = await (
await database.prepare(
"SELECT before_json,after_json FROM audit_log WHERE action='proposal.failed' AND entity_id='canonical-http-failure'",
)
).get();
expect(JSON.stringify(proposal)).not.toContain(sentinel);
expect(JSON.stringify(audit)).not.toContain(sentinel);
expect(mocks.createNotification).toHaveBeenCalledOnce();
expect(JSON.stringify(mocks.createNotification.mock.calls[0])).not.toContain(
sentinel,
);
});
it('marks malformed and unsupported specialization templates failed after replay rollback', async () => {
await insertSpecialization();
for (const [index, descriptionTemplate] of [
'Gain {1 Weapon Damage.',
'Gain {unsupported} Weapon Damage.',
].entries()) {
const proposalId = `specialization-invalid-${index}`;
await insertProposal(
proposalId,
{
id: 'client-effect-id',
_proposedEntityId: 'client-effect-draft',
name: `Invalid Effect ${index}`,
specializationId: 'specialization-route',
descriptionTemplate,
expectedAttachmentFingerprints: '{}',
},
{
module: 'specializations',
action: 'saveCombatEffect',
draftEntityId: `invalid-effect-${index}`,
},
);
const result = await actions.approve?.({
cookies: {} as never,
request: actionRequest(proposalId, { note: 'invalid template' }),
fetch: vi.fn(),
url: new URL('http://localhost/isac/data-moderation'),
} as never);
expect(result).toMatchObject({ status: 409 });
expect(
await (
await database.prepare(
'SELECT status,applied_entity_id FROM data_change_proposals WHERE id=?',
)
).get(proposalId),
).toEqual({ status: 'failed', applied_entity_id: null });
expect(
await (
await database.prepare(
'SELECT id FROM specialization_combat_effects WHERE id=?',
)
).get(`invalid-effect-${index}`),
).toBeUndefined();
const audit = await (
await database.prepare(
"SELECT after_json FROM audit_log WHERE action='proposal.failed' AND entity_id=?",
)
).get(proposalId);
expect(String(audit?.after_json)).not.toContain(descriptionTemplate);
expect(JSON.parse(String(audit?.after_json))).toEqual({
status: 'failed',
messagePresent: true,
});
}
});
it('keeps the committed Phase 2 owner and proposal state when notification fails', async () => {
await insertProtocolProfile();
await insertProposal(
'approval-notification-failure',
{
id: 'client-id',
_proposedEntityId: 'client-draft-id',
name: 'Route Protocol',
quality: 'Standard',
attributeType: 'Firepower',
primaryStatId: 'firepower',
primaryBaseValue: '5',
expectedTalentId: '',
hasTalent: '0',
},
{
module: 'protocols',
action: 'save',
draftEntityId: 'route-protocol-draft',
},
);
mocks.createNotification.mockRejectedValueOnce(
new Error('notification failed'),
);
const result = await actions.approve?.({
cookies: {} as never,
request: actionRequest('approval-notification-failure', {
note: 'approval note',
}),
fetch: vi.fn(),
url: new URL('http://localhost/isac/data-moderation'),
} as never);
expect(result).toMatchObject({
moderationSaved: true,
moderationToken: expect.any(String),
});
expect(
await (
await database.prepare(
'SELECT id,name FROM os_protocols WHERE id=?',
)
).get('route-protocol-draft'),
).toEqual({ id: 'route-protocol-draft', name: 'Route Protocol' });
expect(
await (
await database.prepare(
'SELECT id FROM os_protocols WHERE id IN (?,?)',
)
).all('client-id', 'client-draft-id'),
).toEqual([]);
expect(
await (
await database.prepare(
'SELECT status,applied_entity_id FROM data_change_proposals WHERE id=?',
)
).get('approval-notification-failure'),
).toEqual({ status: 'approved', applied_entity_id: 'route-protocol-draft' });
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='os_protocol.created' AND entity_id=?",
)
).get('route-protocol-draft'),
).toEqual({ count: 1 });
expect(mocks.createNotification).toHaveBeenCalledOnce();
});
});
describe('Data Oversight rejection', () => {
it('commits the conditional rejection and audit before notifying', async () => {
await insertProposal('proposal-1', { name: 'Reject me' });
const result = await actions.reject?.({
cookies: {} as never,
request: actionRequest('proposal-1', { note: 'Needs correction' }),
} as never);
expect(result).toMatchObject({
moderationSaved: true,
moderationToken: expect.any(String),
});
expect(
await (
await database.prepare(
'SELECT status,reviewed_by,review_note FROM data_change_proposals WHERE id=?',
)
).get('proposal-1'),
).toEqual({
status: 'rejected',
reviewed_by: 'reviewer',
review_note: 'Needs correction',
});
const audit = await (
await database.prepare(
"SELECT action,after_json FROM audit_log WHERE entity_id='proposal-1'",
)
).get();
expect(audit?.action).toBe('proposal.rejected');
expect(String(audit?.after_json)).not.toContain('Needs correction');
expect(JSON.parse(String(audit?.after_json))).toEqual({
status: 'rejected',
module: 'attributes',
action: 'save',
proposedBy: 'contributor',
});
expect(mocks.createNotification).toHaveBeenCalledOnce();
});
it('does not overwrite a committed approval or notify after the guard fails', async () => {
await insertProposal(
'approved',
{ name: 'Already approved' },
{ status: 'approved' },
);
const result = await actions.reject?.({
cookies: {} as never,
request: actionRequest('approved', { note: 'Too late' }),
} as never);
expect(result).toMatchObject({ status: 404 });
expect(
await (
await database.prepare(
'SELECT status FROM data_change_proposals WHERE id=?',
)
).get('approved'),
).toEqual({ status: 'approved' });
expect(mocks.createNotification).not.toHaveBeenCalled();
});
it('keeps the committed rejection when the notification fails', async () => {
await insertProposal('notification-failure', { name: 'Notify me' });
mocks.createNotification.mockRejectedValueOnce(
new Error('notification failed'),
);
const result = await actions.reject?.({
cookies: {} as never,
request: actionRequest('notification-failure', {
note: 'Needs correction',
}),
} as never);
expect(result).toMatchObject({ moderationSaved: true });
expect(
await (
await database.prepare(
'SELECT status FROM data_change_proposals WHERE id=?',
)
).get('notification-failure'),
).toEqual({ status: 'rejected' });
});
it('rolls back the status when the rejection audit fails', async () => {
await insertProposal('audit-failure', { name: 'Audit failure' });
await database.exec(`CREATE TRIGGER reject_rejection_audit
BEFORE INSERT ON audit_log WHEN NEW.action='proposal.rejected'
BEGIN SELECT RAISE(ABORT, 'rejection audit failed'); END`);
await expect(
actions.reject?.({
cookies: {} as never,
request: actionRequest('audit-failure', { note: 'Needs correction' }),
} as never),
).rejects.toThrow('rejection audit failed');
expect(
await (
await database.prepare(
'SELECT status FROM data_change_proposals WHERE id=?',
)
).get('audit-failure'),
).toEqual({ status: 'pending' });
expect(mocks.createNotification).not.toHaveBeenCalled();
});
});
describe('Data Oversight correction', () => {
it('replaces dependencies from the current payload in the same transaction', async () => {
await insertProposal(
'dependency-old',
{ name: 'Old' },
{ draftEntityId: 'old-id' },
);
await insertProposal(
'dependency-new',
{ name: 'New' },
{ draftEntityId: 'new-id' },
);
await insertProposal(
'proposal-1',
{ name: 'old-id' },
{ draftEntityId: 'proposal-id' },
);
await (
await database.prepare(
'INSERT INTO data_change_proposal_dependencies(proposal_id,depends_on_proposal_id) VALUES(?,?)',
)
).run('proposal-1', 'dependency-old');
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('proposal-1', {
corrections: JSON.stringify([{ index: 0, value: 'new-id' }]),
}),
} as never);
expect(result).toMatchObject({ moderationSaved: true });
expect(
await (
await database.prepare(
'SELECT depends_on_proposal_id FROM data_change_proposal_dependencies WHERE proposal_id=?',
)
).all('proposal-1'),
).toEqual([{ depends_on_proposal_id: 'dependency-new' }]);
expect(
JSON.parse(
String(
(
await (
await database.prepare(
'SELECT payload_json FROM data_change_proposals WHERE id=?',
)
).get('proposal-1')
)?.payload_json,
),
)[0],
).toMatchObject({ value: 'new-id' });
});
it('returns a controlled conflict and rolls back when corrected dependencies create a cycle', async () => {
await insertProposal(
'cycle-a',
{ name: 'cycle-b-id' },
{ draftEntityId: 'cycle-a-id' },
);
await insertProposal(
'cycle-b',
{ name: 'unrelated-id' },
{ draftEntityId: 'cycle-b-id' },
);
await (
await database.prepare(
'INSERT INTO data_change_proposal_dependencies(proposal_id,depends_on_proposal_id) VALUES(?,?)',
)
).run('cycle-a', 'cycle-b');
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('cycle-b', {
corrections: JSON.stringify([{ index: 0, value: 'cycle-a-id' }]),
}),
} as never);
expect(result).toMatchObject({
status: 409,
data: { moderationError: expect.stringContaining('dependency cycle') },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('cycle-b'),
).toEqual({
payload_json: payload({ name: 'unrelated-id' }),
status: 'pending',
});
expect(
await (
await database.prepare(
'SELECT COUNT(*) AS count FROM data_change_proposal_dependencies WHERE proposal_id=?',
)
).get('cycle-b'),
).toEqual({ count: 0 });
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='proposal.corrected' AND entity_id=?",
)
).get('cycle-b'),
).toEqual({ count: 0 });
});
it('returns 404 when a proposal is no longer pending', async () => {
await insertProposal(
'approved',
{ name: 'Already approved' },
{ status: 'approved' },
);
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('approved', {
corrections: JSON.stringify([{ index: 0, value: 'changed' }]),
}),
} as never);
expect(result).toMatchObject({ status: 404 });
});
it('rejects malformed stored payloads as controlled correction failures', async () => {
await insertProposal('malformed-correction', { name: 'Malformed' });
await (
await database.prepare(
'UPDATE data_change_proposals SET payload_json=? WHERE id=?',
)
).run('not-json', 'malformed-correction');
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('malformed-correction', {
corrections: JSON.stringify([{ index: 0, value: 'changed' }]),
}),
} as never);
expect(result).toMatchObject({
status: 400,
data: { moderationError: 'The stored proposal payload is invalid.' },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('malformed-correction'),
).toEqual({ payload_json: 'not-json', status: 'pending' });
});
it('rejects forged corrections to redacted structured fields without changing proposal dependencies', async () => {
await insertProposal(
'redacted-dependency',
{ name: 'Dependency' },
{ draftEntityId: 'redacted-dependency-id' },
);
const ranksJson = JSON.stringify({
ranks: [{ rank: 1, cost: 100 }],
expectedSourceFingerprint: 'source-fingerprint-sentinel',
expectedAttachmentFingerprints: {
attachment: 'attachment-fingerprint-sentinel',
},
});
await insertProposal(
'redacted-correction',
{ name: 'Ranked record', ranksJson },
{ draftEntityId: 'redacted-record-id' },
);
await (
await database.prepare(
'INSERT INTO data_change_proposal_dependencies(proposal_id,depends_on_proposal_id) VALUES(?,?)',
)
).run('redacted-correction', 'redacted-dependency');
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('redacted-correction', {
corrections: JSON.stringify([
{ index: 1, value: JSON.stringify({ ranks: [{ rank: 99, cost: 1 }] }) },
]),
}),
} as never);
expect(result).toMatchObject({
status: 400,
data: { moderationError: 'Proposal identities cannot be corrected.' },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('redacted-correction'),
).toEqual({
payload_json: payload({ name: 'Ranked record', ranksJson }),
status: 'pending',
});
expect(
await (
await database.prepare(
'SELECT depends_on_proposal_id FROM data_change_proposal_dependencies WHERE proposal_id=?',
)
).all('redacted-correction'),
).toEqual([{ depends_on_proposal_id: 'redacted-dependency' }]);
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='proposal.corrected' AND entity_id=?",
)
).get('redacted-correction'),
).toEqual({ count: 0 });
});
it('rejects corrected values that introduce protected JSON keys', async () => {
const ranksJson = JSON.stringify({ ranks: [{ rank: 1, cost: 100 }] });
await insertProposal('redacted-correction-value', {
name: 'Ranked record',
ranksJson,
});
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('redacted-correction-value', {
corrections: JSON.stringify([
{
index: 1,
value: JSON.stringify({
ranks: [{ rank: 2, cost: 50 }],
expectedSourceFingerprint: 'forged-source-fingerprint',
expectedAttachmentFingerprints: {
attachment: 'forged-attachment-fingerprint',
},
}),
},
]),
}),
} as never);
expect(result).toMatchObject({
status: 400,
data: { moderationError: 'Proposal identities cannot be corrected.' },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('redacted-correction-value'),
).toEqual({
payload_json: payload({ name: 'Ranked record', ranksJson }),
status: 'pending',
});
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='proposal.corrected' AND entity_id=?",
)
).get('redacted-correction-value'),
).toEqual({ count: 0 });
});
it.each([
['protocol', 'protocols', 'save'],
['skill chip', 'skillchips', 'save'],
])('rejects forged expectedTalentId corrections for %s proposals', async (_label, module, action) => {
const proposalId = `immutable-expected-talent-${module}`;
const originalPayload = payload({
name: 'Talent-linked record',
expectedTalentId: 'talent-concurrency-token',
});
await insertProposal(
proposalId,
{
name: 'Talent-linked record',
expectedTalentId: 'talent-concurrency-token',
},
{ module, action },
);
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest(proposalId, {
corrections: JSON.stringify([{ index: 1, value: 'forged-talent-id' }]),
}),
} as never);
expect(result).toMatchObject({
status: 400,
data: { moderationError: 'Proposal identities cannot be corrected.' },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get(proposalId),
).toEqual({ payload_json: originalPayload, status: 'pending' });
});
it('rejects corrections to nested Specialization attachment identities', async () => {
const ranksJson = JSON.stringify([
{
rank: 1,
combatEffects: [{ id: 'attachment-1', effectId: 'effect-1' }],
},
]);
const originalPayload = payload({ name: 'Tree node', ranksJson });
await insertProposal(
'immutable-specialization-ranks',
{ name: 'Tree node', ranksJson },
{ module: 'specializations', action: 'saveNode' },
);
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('immutable-specialization-ranks', {
corrections: JSON.stringify([
{
index: 1,
value: JSON.stringify([
{
rank: 1,
combatEffects: [{ id: '', effectId: 'effect-1' }],
},
]),
},
]),
}),
} as never);
expect(result).toMatchObject({
status: 400,
data: { moderationError: 'Proposal identities cannot be corrected.' },
});
expect(
await (
await database.prepare(
'SELECT payload_json,status FROM data_change_proposals WHERE id=?',
)
).get('immutable-specialization-ranks'),
).toEqual({ payload_json: originalPayload, status: 'pending' });
});
it('rejects corrections to immutable identity entries without changing the payload', async () => {
await insertProposal('immutable-identities', {
id: 'stored-id',
_proposedEntityId: 'stored-draft-id',
name: 'Original',
});
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('immutable-identities', {
corrections: JSON.stringify([
{ index: 0, value: 'forged-id' },
{ index: 1, value: 'forged-draft-id' },
]),
}),
} as never);
expect(result).toMatchObject({ status: 400 });
expect(
JSON.parse(
String(
(
await (
await database.prepare(
'SELECT payload_json FROM data_change_proposals WHERE id=?',
)
).get('immutable-identities')
)?.payload_json,
),
),
).toEqual([
{ name: 'id', kind: 'text', value: 'stored-id' },
{ name: '_proposedEntityId', kind: 'text', value: 'stored-draft-id' },
{ name: 'name', kind: 'text', value: 'Original' },
]);
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='proposal.corrected'",
)
).get(),
).toEqual({ count: 0 });
});
it('records correction indexes and bounded lengths without corrected values', async () => {
await insertProposal('correction-audit', { name: 'Original' });
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('correction-audit', {
corrections: JSON.stringify([
{ index: 0, value: 'correction-value-sentinel' },
]),
}),
} as never);
expect(result).toMatchObject({ moderationSaved: true });
const audit = await (
await database.prepare(
"SELECT before_json,after_json FROM audit_log WHERE action='proposal.corrected' AND entity_id='correction-audit'",
)
).get();
expect(JSON.stringify(audit)).not.toContain('correction-value-sentinel');
expect(JSON.parse(String(audit?.before_json))).toMatchObject({
fields: [
{ index: 0, name: 'name', changed: true, valueLength: 8 },
],
});
expect(JSON.parse(String(audit?.after_json))).toMatchObject({
fields: [
{ index: 0, name: 'name', changed: true, valueLength: 25 },
],
});
});
it('rejects duplicate correction indexes without changing the payload', async () => {
await insertProposal('duplicate', { name: 'Original' });
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('duplicate', {
corrections: JSON.stringify([
{ index: 0, value: 'first' },
{ index: 0, value: 'second' },
]),
}),
} as never);
expect(result).toMatchObject({ status: 400 });
expect(
JSON.parse(
String(
(
await (
await database.prepare(
'SELECT payload_json FROM data_change_proposals WHERE id=?',
)
).get('duplicate')
)?.payload_json,
),
)[0],
).toMatchObject({ value: 'Original' });
expect(
await (
await database.prepare(
"SELECT COUNT(*) AS count FROM audit_log WHERE action='proposal.corrected'",
)
).get(),
).toEqual({ count: 0 });
});
it('rejects corrections that would exceed the aggregate text bound', async () => {
const fields = Object.fromEntries(
Array.from({ length: 20 }, (_, index) => [
`field-${index}`,
'x'.repeat(100_000),
]),
);
fields.last = 'x';
await insertProposal('aggregate', fields);
const result = await actions.correct?.({
cookies: {} as never,
request: actionRequest('aggregate', {
corrections: JSON.stringify([
{ index: 20, value: 'x'.repeat(100_000) },
]),
}),
} as never);
expect(result).toMatchObject({ status: 400 });
expect(
JSON.parse(
String(
(
await (
await database.prepare(
'SELECT payload_json FROM data_change_proposals WHERE id=?',
)
).get('aggregate')
)?.payload_json,
),
)[20],
).toMatchObject({ value: 'x' });
});
it('rolls back payload, dependencies, and audit when correction audit fails', async () => {
await insertProposal(
'dependency',
{ name: 'Dependency' },
{ draftEntityId: 'dependency-id' },
);
await insertProposal('proposal-1', { name: 'dependency-id' });
await (
await database.prepare(
'INSERT INTO data_change_proposal_dependencies(proposal_id,depends_on_proposal_id) VALUES(?,?)',
)
).run('proposal-1', 'dependency');
await database.exec(`CREATE TRIGGER reject_correction_audit
BEFORE INSERT ON audit_log WHEN NEW.action='proposal.corrected'
BEGIN SELECT RAISE(ABORT, 'correction audit failed'); END`);
await expect(
actions.correct?.({
cookies: {} as never,
request: actionRequest('proposal-1', {
corrections: JSON.stringify([{ index: 0, value: 'changed' }]),
}),
} as never),
).rejects.toThrow('correction audit failed');
expect(
JSON.parse(
String(
(
await (
await database.prepare(
'SELECT payload_json FROM data_change_proposals WHERE id=?',
)
).get('proposal-1')
)?.payload_json,
),
)[0],
).toMatchObject({ value: 'dependency-id' });
expect(
await (
await database.prepare(
'SELECT depends_on_proposal_id FROM data_change_proposal_dependencies WHERE proposal_id=?',
)
).all('proposal-1'),
).toEqual([{ depends_on_proposal_id: 'dependency' }]);
});
});