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) => JSON.stringify( Object.entries(fields).map(([name, value]) => ({ name, kind: 'text', value, })), ); const insertProposal = async ( id: string, fields: Record, 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) => { 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' }]); }); });