openvibers/openvibe.deals Commits

A moderator's flag decision commits together with its audit row: logAction() inside flags.resolve's transaction was not awaited, so the moderation_log write floated outside the awaited flow and a failing audit write was swallowed (under Node 22 an unhandled rejection is fatal). It is awaited now; test/flag-audit.test.js fails without the fix (resolve returns before the audit row exists; a failing audit write must reject the decision). Found by the estate-wide floating-promise sweep, fixed and reviewed independently (VERDICT: SHIP). npm test 15/15 test files passed.

OP OpenVibers committed 29 Sept 2026 · 57b41944aa · 2 files, +49 −1


Changes

ddiff --git a/server/domain/flags.js b/server/domain/flags.js
iindex d1a2c21..3773149 100644
---- a/server/domain/flags.js
++++ b/server/domain/flags.js
@@@ -72,7 +72,7 @@ function createFlags({ config, store, reads, limits, access, publication, logAct
const actor = viewer.subject || viewer.service;
return await store.tx(async () => {
await q.resolve.run(status, actor, store.now(), text(resolution, { max: 300 }), store.now(), f.id);
-- logAction(`flag.${status === 'resolved' ? 'resolve' : 'dismiss'}`, { offerId: f.offer_id, actor, reason: resolution || null, before: { flag: f.id, status: 'open' }, after: { status } });
++ await logAction(`flag.${status === 'resolved' ? 'resolve' : 'dismiss'}`, { offerId: f.offer_id, actor, reason: resolution || null, before: { flag: f.id, status: 'open' }, after: { status } });
// A moderator's decision on a report goes to Network's moderation audit log (ADR-022).
if (outbox) {
const person = viewer.subject && /^usr_/.test(viewer.subject) ? viewer.subject : null;
ddiff --git a/test/flag-audit.test.js b/test/flag-audit.test.js
nnew file mode 100644
iindex 0000000..e5d63a6
---- /dev/null
++++ b/test/flag-audit.test.js
@@@ -0,0 +1,48 @@
++'use strict';
++/**
++ * A moderator's decision and its moderation_log audit row are one transaction. flags.resolve must
++ * await logAction, not leave the write floating: the decision must not return before the audit row
++ * is in, and an audit write that fails must fail the decision (rolling the resolve back), not be
++ * swallowed as an unhandled rejection.
++ */
++const assert = require('assert');
++const { check, done } = require('./helpers/boot');
++const { createFlags } = require('../server/domain/flags');
++
++/** The flags domain with just enough store to resolve one open flag; logAction is the observed part. */
++function harness({ logAction }) {
++ const flag = { id: 'dfl_1', offer_id: 'dor_1', kind: 'spam', origin: 'user', reporter: 'usr_r', reason: null, details: '{}' };
++ let status = 'open';
++ const store = {
++ db: {
++ prepare: () => ({
++ get: async (id) => (String(id) === flag.id ? { ...flag, status } : undefined),
++ run: async () => { status = 'resolved'; return { rowCount: 1 }; },
++ all: async () => [],
++ }),
++ },
++ tx: async (fn) => await fn(),
++ now: () => 1700000000000,
++ };
++ return createFlags({ config: {}, store, reads: {}, limits: {}, access: { isModerator: () => true }, publication: {}, logAction });
++}
++
++const moderator = { subject: 'usr_mod', kind: 'user' };
++
++(async () => {
++ await check('resolving a flag awaits its moderation_log audit write inside the transaction', async () => {
++ let wrote = null;
++ const flags = harness({ logAction: async (action) => { await new Promise((r) => setImmediate(r)); wrote = action; } });
++ const out = await flags.resolve(moderator, 'dfl_1', { status: 'resolved', resolution: 'checked' });
++ assert.strictEqual(out.changed, true);
++ assert.strictEqual(wrote, 'flag.resolve', 'logAction must finish before resolve() returns');
++ });
++
++ await check('an audit write that fails fails the moderator decision instead of being swallowed', async () => {
++ const failure = new Error('moderation_log is unavailable');
++ const flags = harness({ logAction: () => { const p = Promise.reject(failure); p.catch(() => {}); return p; } });
++ await assert.rejects(() => flags.resolve(moderator, 'dfl_1', { status: 'dismissed', resolution: 'nope' }), /moderation_log is unavailable/);
++ });
++
++ done();
++})();