openvibers/openvibe.tips Commits
Pins for the switch: openvibe-sdk v0.18.0 (from v0.15.0; ambient transactions, db.prepare, JSON null verification) and openvibe-contracts v0.76.0 (from v0.49.0, ahead of October's pin-drift failure). Two tests made exact. settlement's paid-effects check drained due transfers once after a 100 ms sleep, and failed about one run in five through PgBouncer when a background pass held a transfer's lease (it failed the same with ambient transactions off). It now drains until all three exist, within 3 s. sqlite-import expected SDK 0.15's JSON null mismatch; 0.18 verifies it. STATUS.json and the README record the pins and the production switch. npm test and npm run test:pg: 18/18 each; settlement stable over repeated runs.
Changes
@@@ -48,7 +48,7 @@ from *interaction delivered*.- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer- **OpenVibe.Shared** v1.25.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and-- the Network's `navbar.js`; **openvibe-contracts** v0.49.0 and **openvibe-sdk** v0.15.0 (service tokens,++ the Network's `navbar.js`; **openvibe-contracts** v0.76.0 and **openvibe-sdk** v0.18.0 (service tokens,the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox, Valkey, pub/sub and the sharedlimit store), pinned by release tarball; `pg` and `iovalkey` (drivers), PGlite for tests@@@ -451,7 +451,7 @@ exist here (plan §12.12):4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing andEvents (Billing is in shadow and has sent no events; production holds the Live import and no creatorprofile);--5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 and v0.32.0 (v0.49.0 pinned);++5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 and v0.32.0 (v0.76.0 pinned);6. a migration/seed strategy ✔, a written threat review ✔ ([docs/threat-review.md](docs/threat-review.md),internal; an independent review is still due), and sitemap/robots ✔ (no feed);7. acceptance tests proving the advertised functionality — ✔ (table above).@@@ -8,8 +8,8 @@"authoritative": false,"deployed": true,"deployment": "Deployed internally, not launched: deployed with `sudo ovhost deploy tips`, /opt/openvibe.tips, unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscriptions billing.transaction.* and billing.receipt.* -> /internal/events (secret rotated 2026-09-24); TIPS_CHAT_ADAPTER is empty (no chat side effects).",-- "database": "PostgreSQL 18 through PgBouncer (transaction mode) as the system of record, Valkey 9 for shared state (per-actor limits, overlay fan-out, overlay stream slots), ADR-035: database ov_tips from OpenVibe.Host's data role, schema in migrations/ (0001_initial, expand), async on openvibe-sdk/db v0.15.0, money paths serializable with row locks, PostgreSQL outbox and inbox. The deployed release still runs on SQLite (/var/lib/openvibe-tips/tips.db) until the switch in README Deploy: scripts/migrate-to-postgres.js imports it verified by counts and checksums (rehearsed on the SQLite databases the previous release makes: all tables and columns, report.ok), and the file stays read-only for the 7-day N-1 window.",-- "contracts": "openvibe-contracts v0.49.0",++ "database": "PostgreSQL 18 through PgBouncer (transaction mode) as the system of record, Valkey 9 for shared state (per-actor limits, overlay fan-out, overlay stream slots), ADR-035: database ov_tips from OpenVibe.Host's data role, schema in migrations/ (0001_initial, expand), async on openvibe-sdk/db v0.18.0, money paths serializable with row locks, PostgreSQL outbox and inbox. Switched in production on 2026-09-28: scripts/migrate-to-postgres.js imported /var/lib/openvibe-tips/tips.db verified by counts and checksums (rehearsed on a copy of the production file first: report.ok, 139 interactions, 143 migration maps, 4 goals); the SQLite file stays read-only for the 7-day N-1 window (until 2026-10-05), then is archived to B2.",++ "contracts": "openvibe-contracts v0.76.0","notes": "Runtime, API, SSR pages, overlays, the Billing settlement consumer, events outbox, Live import and reconciliation are built, tested and deployed, together with /metrics (loopback only), truthful /api/ready (Billing and Events checks), supporter privacy (anonymous, hidden amount, private message), goal and supporter-page settings, export and erasure (tips.interaction.erased), moderation (word filter, hide/show, invited moderators, tips.interaction.moderated) and per-actor limits. Capabilities (18 tips.*) and payload schemas are in openvibe-contracts. On 2026-09-24 production held the Live import (3 import runs, the last 2026-09-23 18:31 UTC: 139 interactions, 4 goals, 143 migration maps); 0 creator profiles and 0 overlay tokens (nobody had set up the product). No billing.* event had arrived: Billing runs in shadow. A written internal threat review (docs/threat-review.md) with its fixes and test/security.test.js. Not done: an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.","updated": "2026-09-28","features": [@@@ -15,8 +15,8 @@"express": "^4.21.2","iovalkey": "^0.4.0","jsonwebtoken": "^9.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0","pg": "^8.23.0"},@@@ -1074,9 +1074,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.49.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",-- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",++ "version": "0.76.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",++ "integrity": "sha512-zTdx776KWrcJKm1JTnf+0+BnsvswdvXy1ldPyse1hSqhnW/SSr98sX696eRetFUhTl8qVM1xSRCykqIUbqItPw==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1090,9 +1090,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.15.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",-- "integrity": "sha512-5ZcqEZxRQjT15erqtEdJ4js3roSijUFiQZPbGIHpaOVFRJSI45ZCEgTBb9ByKNRdUNBl4ub9Vb1dF2CcCVJjrg==",++ "version": "0.18.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0",++ "integrity": "sha512-Jh+lMx35cWM1byXUqt0pb3mYdOjkHneYIPOzKlHdYFhzmlUSGLlLF1WJviJxxbZ0XVy/0jTcZ38OCPGxOz7Aeg==","license": "MIT","engines": {"node": ">=20"@@@ -29,8 +29,8 @@"express": "^4.21.2","iovalkey": "^0.4.0","jsonwebtoken": "^9.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0","pg": "^8.23.0"},@@@ -208,8 +208,12 @@ const { boot, check, done } = require('./helpers/app');const count = () => t.db.value('SELECT (SELECT count(*) FROM paid_messages WHERE interaction_id = ANY($1)) + (SELECT count(*) FROM paid_media_requests WHERE interaction_id = ANY($1))', [ids]);assert.strictEqual(await count(), 0);for (const id of ids) await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));-- await new Promise((ok) => setTimeout(ok, 100));-- await domain.interactions.processDueTransfers();++ // A background pass may hold a transfer's lease for a moment (FOR UPDATE SKIP LOCKED): drain until all++ // three are done, within a deadline, instead of trusting one pass after a fixed sleep.++ for (const until = Date.now() + 3000; (await count()) < 3 && Date.now() < until;) {++ await domain.interactions.processDueTransfers();++ await new Promise((ok) => setTimeout(ok, 50));++ }assert.strictEqual(await count(), 3);const pm = await t.db.maybe('SELECT * FROM paid_messages WHERE interaction_id = $1', [ids[0]]);assert.strictEqual(pm.highlight_seconds, 60);@@@ -140,13 +140,13 @@ function fixture(file) {assert.strictEqual(await db.value("INSERT INTO tips_event_outbox (event_id, envelope, created_at) VALUES ('evt_03', '{}', 1) RETURNING id"), 3);});-- await check('a rehearsal can be repeated (truncate), and a value the checksum would miss is caught', async () => {++ await check('a rehearsal can be repeated (truncate), and JSON null verifies without a mapping', async () => {const again = await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });assert.strictEqual(again.ok, true, JSON.stringify(again.problems));-- // Without the 'null' mapping, JSON null and SQL NULL differ: the verification says so.-- const bad = await importSqlite({ sqlite: file, db, truncate: true, tables: { ...TABLES, paid_messages: {} }, only: ['paid_messages', 'tip_interactions', 'creator_tip_profiles'], log: quiet });-- assert.strictEqual(bad.ok, false);-- assert.match(bad.problems.map((x) => `${x.table}: ${x.problem}`).join('\n'), /paid_messages: verification failed/);++ // openvibe-sdk ≥ 0.18 counts JSON null and SQL NULL as equal in verification, so the import is verified++ // even without Tips' own 'null' mapping (which stays: it stores SQL NULL, not a JSON null).++ const plain = await importSqlite({ sqlite: file, db, truncate: true, tables: { ...TABLES, paid_messages: {} }, only: ['paid_messages', 'tip_interactions', 'creator_tip_profiles'], log: quiet });++ assert.strictEqual(plain.ok, true, JSON.stringify(plain.problems));await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });});
@@ -48,7 +48,7 @@ from *interaction delivered*.
@@ -48,7 +48,7 @@ from *interaction delivered*.
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
- **OpenVibe.Shared** v1.25.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
- **OpenVibe.Shared** v1.25.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
- the Network's `navbar.js`; **openvibe-contracts** v0.49.0 and **openvibe-sdk** v0.15.0 (service tokens,
+ the Network's `navbar.js`; **openvibe-contracts** v0.76.0 and **openvibe-sdk** v0.18.0 (service tokens,
the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox, Valkey, pub/sub and the shared
the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox, Valkey, pub/sub and the shared
limit store), pinned by release tarball; `pg` and `iovalkey` (drivers), PGlite for tests
limit store), pinned by release tarball; `pg` and `iovalkey` (drivers), PGlite for tests
@@ -451,7 +451,7 @@ exist here (plan §12.12):
@@ -451,7 +451,7 @@ exist here (plan §12.12):
4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing and
4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing and
Events (Billing is in shadow and has sent no events; production holds the Live import and no creator
Events (Billing is in shadow and has sent no events; production holds the Live import and no creator
profile);
profile);
-5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 and v0.32.0 (v0.49.0 pinned);
+5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 and v0.32.0 (v0.76.0 pinned);
6. a migration/seed strategy ✔, a written threat review ✔ ([docs/threat-review.md](docs/threat-review.md),
6. a migration/seed strategy ✔, a written threat review ✔ ([docs/threat-review.md](docs/threat-review.md),
internal; an independent review is still due), and sitemap/robots ✔ (no feed);
internal; an independent review is still due), and sitemap/robots ✔ (no feed);
7. acceptance tests proving the advertised functionality — ✔ (table above).
7. acceptance tests proving the advertised functionality — ✔ (table above).
@@ -8,8 +8,8 @@
@@ -8,8 +8,8 @@
"authoritative": false,
"authoritative": false,
"deployed": true,
"deployed": true,
"deployment": "Deployed internally, not launched: deployed with `sudo ovhost deploy tips`, /opt/openvibe.tips, unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscriptions billing.transaction.* and billing.receipt.* -> /internal/events (secret rotated 2026-09-24); TIPS_CHAT_ADAPTER is empty (no chat side effects).",
"deployment": "Deployed internally, not launched: deployed with `sudo ovhost deploy tips`, /opt/openvibe.tips, unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscriptions billing.transaction.* and billing.receipt.* -> /internal/events (secret rotated 2026-09-24); TIPS_CHAT_ADAPTER is empty (no chat side effects).",
- "database": "PostgreSQL 18 through PgBouncer (transaction mode) as the system of record, Valkey 9 for shared state (per-actor limits, overlay fan-out, overlay stream slots), ADR-035: database ov_tips from OpenVibe.Host's data role, schema in migrations/ (0001_initial, expand), async on openvibe-sdk/db v0.15.0, money paths serializable with row locks, PostgreSQL outbox and inbox. The deployed release still runs on SQLite (/var/lib/openvibe-tips/tips.db) until the switch in README Deploy: scripts/migrate-to-postgres.js imports it verified by counts and checksums (rehearsed on the SQLite databases the previous release makes: all tables and columns, report.ok), and the file stays read-only for the 7-day N-1 window.",
- "contracts": "openvibe-contracts v0.49.0",
+ "database": "PostgreSQL 18 through PgBouncer (transaction mode) as the system of record, Valkey 9 for shared state (per-actor limits, overlay fan-out, overlay stream slots), ADR-035: database ov_tips from OpenVibe.Host's data role, schema in migrations/ (0001_initial, expand), async on openvibe-sdk/db v0.18.0, money paths serializable with row locks, PostgreSQL outbox and inbox. Switched in production on 2026-09-28: scripts/migrate-to-postgres.js imported /var/lib/openvibe-tips/tips.db verified by counts and checksums (rehearsed on a copy of the production file first: report.ok, 139 interactions, 143 migration maps, 4 goals); the SQLite file stays read-only for the 7-day N-1 window (until 2026-10-05), then is archived to B2.",
+ "contracts": "openvibe-contracts v0.76.0",
"notes": "Runtime, API, SSR pages, overlays, the Billing settlement consumer, events outbox, Live import and reconciliation are built, tested and deployed, together with /metrics (loopback only), truthful /api/ready (Billing and Events checks), supporter privacy (anonymous, hidden amount, private message), goal and supporter-page settings, export and erasure (tips.interaction.erased), moderation (word filter, hide/show, invited moderators, tips.interaction.moderated) and per-actor limits. Capabilities (18 tips.*) and payload schemas are in openvibe-contracts. On 2026-09-24 production held the Live import (3 import runs, the last 2026-09-23 18:31 UTC: 139 interactions, 4 goals, 143 migration maps); 0 creator profiles and 0 overlay tokens (nobody had set up the product). No billing.* event had arrived: Billing runs in shadow. A written internal threat review (docs/threat-review.md) with its fixes and test/security.test.js. Not done: an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
"notes": "Runtime, API, SSR pages, overlays, the Billing settlement consumer, events outbox, Live import and reconciliation are built, tested and deployed, together with /metrics (loopback only), truthful /api/ready (Billing and Events checks), supporter privacy (anonymous, hidden amount, private message), goal and supporter-page settings, export and erasure (tips.interaction.erased), moderation (word filter, hide/show, invited moderators, tips.interaction.moderated) and per-actor limits. Capabilities (18 tips.*) and payload schemas are in openvibe-contracts. On 2026-09-24 production held the Live import (3 import runs, the last 2026-09-23 18:31 UTC: 139 interactions, 4 goals, 143 migration maps); 0 creator profiles and 0 overlay tokens (nobody had set up the product). No billing.* event had arrived: Billing runs in shadow. A written internal threat review (docs/threat-review.md) with its fixes and test/security.test.js. Not done: an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
"updated": "2026-09-28",
"updated": "2026-09-28",
"features": [
"features": [
@@ -15,8 +15,8 @@
@@ -15,8 +15,8 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1074,9 +1074,9 @@
@@ -1074,9 +1074,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.49.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",
+ "version": "0.76.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
+ "integrity": "sha512-zTdx776KWrcJKm1JTnf+0+BnsvswdvXy1ldPyse1hSqhnW/SSr98sX696eRetFUhTl8qVM1xSRCykqIUbqItPw==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1090,9 +1090,9 @@
@@ -1090,9 +1090,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.15.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",
- "integrity": "sha512-5ZcqEZxRQjT15erqtEdJ4js3roSijUFiQZPbGIHpaOVFRJSI45ZCEgTBb9ByKNRdUNBl4ub9Vb1dF2CcCVJjrg==",
+ "version": "0.18.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0",
+ "integrity": "sha512-Jh+lMx35cWM1byXUqt0pb3mYdOjkHneYIPOzKlHdYFhzmlUSGLlLF1WJviJxxbZ0XVy/0jTcZ38OCPGxOz7Aeg==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -29,8 +29,8 @@
@@ -29,8 +29,8 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.15.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.18.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -208,8 +208,12 @@ const { boot, check, done } = require('./helpers/app');
@@ -208,8 +208,12 @@ const { boot, check, done } = require('./helpers/app');
const count = () => t.db.value('SELECT (SELECT count(*) FROM paid_messages WHERE interaction_id = ANY($1)) + (SELECT count(*) FROM paid_media_requests WHERE interaction_id = ANY($1))', [ids]);
const count = () => t.db.value('SELECT (SELECT count(*) FROM paid_messages WHERE interaction_id = ANY($1)) + (SELECT count(*) FROM paid_media_requests WHERE interaction_id = ANY($1))', [ids]);
assert.strictEqual(await count(), 0);
assert.strictEqual(await count(), 0);
for (const id of ids) await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));
for (const id of ids) await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));
- await new Promise((ok) => setTimeout(ok, 100));
- await domain.interactions.processDueTransfers();
+ // A background pass may hold a transfer's lease for a moment (FOR UPDATE SKIP LOCKED): drain until all
+ // three are done, within a deadline, instead of trusting one pass after a fixed sleep.
+ for (const until = Date.now() + 3000; (await count()) < 3 && Date.now() < until;) {
+ await domain.interactions.processDueTransfers();
+ await new Promise((ok) => setTimeout(ok, 50));
+ }
assert.strictEqual(await count(), 3);
assert.strictEqual(await count(), 3);
const pm = await t.db.maybe('SELECT * FROM paid_messages WHERE interaction_id = $1', [ids[0]]);
const pm = await t.db.maybe('SELECT * FROM paid_messages WHERE interaction_id = $1', [ids[0]]);
assert.strictEqual(pm.highlight_seconds, 60);
assert.strictEqual(pm.highlight_seconds, 60);
@@ -140,13 +140,13 @@ function fixture(file) {
@@ -140,13 +140,13 @@ function fixture(file) {
assert.strictEqual(await db.value("INSERT INTO tips_event_outbox (event_id, envelope, created_at) VALUES ('evt_03', '{}', 1) RETURNING id"), 3);
assert.strictEqual(await db.value("INSERT INTO tips_event_outbox (event_id, envelope, created_at) VALUES ('evt_03', '{}', 1) RETURNING id"), 3);
});
});
- await check('a rehearsal can be repeated (truncate), and a value the checksum would miss is caught', async () => {
+ await check('a rehearsal can be repeated (truncate), and JSON null verifies without a mapping', async () => {
const again = await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });
const again = await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });
assert.strictEqual(again.ok, true, JSON.stringify(again.problems));
assert.strictEqual(again.ok, true, JSON.stringify(again.problems));
- // Without the 'null' mapping, JSON null and SQL NULL differ: the verification says so.
- const bad = await importSqlite({ sqlite: file, db, truncate: true, tables: { ...TABLES, paid_messages: {} }, only: ['paid_messages', 'tip_interactions', 'creator_tip_profiles'], log: quiet });
- assert.strictEqual(bad.ok, false);
- assert.match(bad.problems.map((x) => `${x.table}: ${x.problem}`).join('\n'), /paid_messages: verification failed/);
+ // openvibe-sdk ≥ 0.18 counts JSON null and SQL NULL as equal in verification, so the import is verified
+ // even without Tips' own 'null' mapping (which stays: it stores SQL NULL, not a JSON null).
+ const plain = await importSqlite({ sqlite: file, db, truncate: true, tables: { ...TABLES, paid_messages: {} }, only: ['paid_messages', 'tip_interactions', 'creator_tip_profiles'], log: quiet });
+ assert.strictEqual(plain.ok, true, JSON.stringify(plain.problems));
await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });
await importSqlite({ sqlite: file, db, truncate: true, tables: TABLES, log: quiet });
});
});