@bongos/core 1.21.106 → 1.21.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +47 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +6 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +5 -3
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_275_claims_auto_pickup.sql +24 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/lifecycle/autopickup.js +94 -0
- package/modules/lifecycle/db-claims.js +18 -4
- package/modules/lifecycle/routes/claims.js +12 -1
- package/modules/lifecycle/routes/project-settings.js +59 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/government-walk.js +1 -0
- package/scripts/gds/run-unit-tests.js +3 -0
- package/src/bongos/project-settings.js +79 -0
- package/src/module-api.js +8 -1
- package/tests/autopickup_cap.mjs +236 -0
- package/tests/autopickup_cap_db.mjs +115 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// tests/autopickup_cap_db.mjs — the auto-pickup cap holds under a race, against a
|
|
2
|
+
// REAL Postgres (task 1004677 / BV2.TW19a, ADR 0371 D5).
|
|
3
|
+
//
|
|
4
|
+
// tests/autopickup_cap.mjs proves the refusal ladder DB-free. What it cannot prove
|
|
5
|
+
// is the claim this file exists for: two runners racing for the LAST slot cannot
|
|
6
|
+
// both get it. That is a property of pg_advisory_xact_lock plus the count running
|
|
7
|
+
// inside the claim's transaction, so it needs a real database and two real
|
|
8
|
+
// connections.
|
|
9
|
+
//
|
|
10
|
+
// It SELF-SKIPS (exit 0) with no reachable Postgres, and is listed in
|
|
11
|
+
// run-unit-tests.js's INTEGRATION set so the DB-free lane never runs it. It builds
|
|
12
|
+
// throwaway `tasks` and `claims` tables, so point it at a scratch database.
|
|
13
|
+
//
|
|
14
|
+
// Run: DATABASE_URL=postgres://... node tests/autopickup_cap_db.mjs
|
|
15
|
+
|
|
16
|
+
import pg from 'pg';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import url from 'node:url';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { strict as assert } from 'node:assert';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await pool.query('SELECT 1');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.log(`autopickup_cap_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
30
|
+
await pool.end().catch(() => {});
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
const { rows: realRows } = await pool.query(`SELECT to_regclass('public.builders') IS NOT NULL AS has_builders`);
|
|
34
|
+
if (realRows[0].has_builders) {
|
|
35
|
+
console.log('autopickup_cap_db.mjs SKIPPED — this database has a `builders` table, so it is not a scratch DB.');
|
|
36
|
+
await pool.end().catch(() => {});
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let passed = 0, failed = 0;
|
|
41
|
+
async function t(name, fn) {
|
|
42
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
43
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await pool.query(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS tasks (id bigint PRIMARY KEY, source text);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
50
|
+
id bigserial PRIMARY KEY, task_id bigint, builder_id bigint,
|
|
51
|
+
claimed_at timestamptz DEFAULT now(), released_at timestamptz);
|
|
52
|
+
`);
|
|
53
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
54
|
+
await pool.query(fs.readFileSync(path.join(HERE, '..', 'migrations', 'core_275_claims_auto_pickup.sql'), 'utf8'));
|
|
55
|
+
|
|
56
|
+
const { autoPickupRefusal, countThisMonth } = require('../modules/lifecycle/autopickup.js');
|
|
57
|
+
|
|
58
|
+
async function reset() {
|
|
59
|
+
await pool.query('TRUNCATE claims, tasks');
|
|
60
|
+
await pool.query(`INSERT INTO tasks (id, source) VALUES (1,'page-tweak'),(2,'page-tweak'),(3,'page-tweak'),(4,'manual')`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// One marked claim the way claimTask makes it: BEGIN, the gate (which takes the
|
|
64
|
+
// lock and counts), the insert, COMMIT. `pause` holds the transaction open after
|
|
65
|
+
// the count so the other racer's count is forced to happen while it is in flight.
|
|
66
|
+
async function markedClaim(taskId, settings, pause = 0) {
|
|
67
|
+
const client = await pool.connect();
|
|
68
|
+
try {
|
|
69
|
+
await client.query('BEGIN');
|
|
70
|
+
const refusal = await autoPickupRefusal(client, { id: taskId, source: 'page-tweak' }, settings);
|
|
71
|
+
if (refusal) { await client.query('ROLLBACK'); return refusal.code; }
|
|
72
|
+
if (pause) await new Promise((r) => setTimeout(r, pause));
|
|
73
|
+
await client.query('INSERT INTO claims (task_id, builder_id, auto_pickup) VALUES ($1, 1, TRUE)', [taskId]);
|
|
74
|
+
await client.query('COMMIT');
|
|
75
|
+
return 'claimed';
|
|
76
|
+
} catch (e) {
|
|
77
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
78
|
+
throw e;
|
|
79
|
+
} finally { client.release(); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await t('core_275 adds a nullable auto_pickup column', async () => {
|
|
83
|
+
const { rows } = await pool.query(`SELECT is_nullable, data_type FROM information_schema.columns WHERE table_name='claims' AND column_name='auto_pickup'`);
|
|
84
|
+
assert.equal(rows[0].data_type, 'boolean');
|
|
85
|
+
assert.equal(rows[0].is_nullable, 'YES');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await t('two runners racing for the last slot: exactly one wins', async () => {
|
|
89
|
+
await reset();
|
|
90
|
+
await pool.query(`INSERT INTO claims (task_id, builder_id, auto_pickup) VALUES (3, 1, TRUE)`); // cap 2, one used
|
|
91
|
+
const settings = { mode: 'on', cap: 2 };
|
|
92
|
+
const results = await Promise.all([markedClaim(1, settings, 300), markedClaim(2, settings, 300)]);
|
|
93
|
+
assert.deepEqual(results.sort(), ['AUTO_PICKUP_CAP_REACHED', 'claimed'], JSON.stringify(results));
|
|
94
|
+
assert.equal(await countThisMonth(pool), 2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await t('a round applied twice automatically refuses a third marked claim', async () => {
|
|
98
|
+
await reset();
|
|
99
|
+
const settings = { mode: 'on', cap: 20 };
|
|
100
|
+
assert.equal(await markedClaim(1, settings), 'claimed');
|
|
101
|
+
assert.equal(await markedClaim(1, settings), 'claimed');
|
|
102
|
+
assert.equal(await markedClaim(1, settings), 'AUTO_PICKUP_ROUND_LIMIT');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t('unmarked claims and last month\'s marked claims do not count', async () => {
|
|
106
|
+
await reset();
|
|
107
|
+
await pool.query(`INSERT INTO claims (task_id, builder_id, auto_pickup, claimed_at) VALUES
|
|
108
|
+
(1, 1, NULL, now()), (2, 1, FALSE, now()), (3, 1, TRUE, date_trunc('month', now()) - interval '1 day'),
|
|
109
|
+
(4, 1, TRUE, now())`); // task 4 is not a page tweak
|
|
110
|
+
assert.equal(await countThisMonth(pool), 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await pool.end();
|
|
114
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
115
|
+
process.exit(failed ? 1 : 0); // the doorway's own pool would otherwise hold the process open
|
|
@@ -242,6 +242,7 @@ const BAD = 'x';
|
|
|
242
242
|
const REQUESTS = {
|
|
243
243
|
'PATCH /project-settings/rot-days': ['PATCH', '/project-settings/rot-days', { days: 'soon' }],
|
|
244
244
|
'PATCH /project-settings/ship-mode': ['PATCH', '/project-settings/ship-mode', { mode: 'sometimes' }],
|
|
245
|
+
'PATCH /project-settings/tweak-autopickup': ['PATCH', '/project-settings/tweak-autopickup', { mode: 'sometimes' }],
|
|
245
246
|
'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
|
|
246
247
|
'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
|
|
247
248
|
'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
|