@blamejs/blamejs-shop 0.4.87 → 0.4.88

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/SECURITY.md +8 -0
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/vendor/MANIFEST.json +53 -31
  5. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +7 -2
  6. package/lib/vendor/blamejs/.github/dependabot.yml +12 -0
  7. package/lib/vendor/blamejs/.github/workflows/ci.yml +16 -12
  8. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +3 -1
  9. package/lib/vendor/blamejs/.github/workflows/release-container.yml +23 -0
  10. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  12. package/lib/vendor/blamejs/examples/wiki/Dockerfile +19 -2
  13. package/lib/vendor/blamejs/lib/atomic-file.js +32 -9
  14. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +105 -40
  15. package/lib/vendor/blamejs/lib/middleware/dpop.js +54 -31
  16. package/lib/vendor/blamejs/lib/middleware/span-http-server.js +7 -0
  17. package/lib/vendor/blamejs/lib/network-tls.js +12 -2
  18. package/lib/vendor/blamejs/lib/queue-local.js +123 -46
  19. package/lib/vendor/blamejs/lib/queue.js +13 -9
  20. package/lib/vendor/blamejs/lib/request-helpers.js +90 -0
  21. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +69 -7
  22. package/lib/vendor/blamejs/lib/self-update.js +11 -2
  23. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
  24. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +6 -2
  25. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +689 -688
  26. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +5 -0
  27. package/lib/vendor/blamejs/package.json +1 -1
  28. package/lib/vendor/blamejs/release-notes/v0.15.17.json +52 -0
  29. package/lib/vendor/blamejs/release-notes/v0.15.18.json +49 -0
  30. package/lib/vendor/blamejs/release-notes/v0.15.19.json +18 -0
  31. package/lib/vendor/blamejs/scripts/check-vendor-currency.js +24 -0
  32. package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +419 -0
  33. package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +471 -0
  34. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +309 -0
  35. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -8
  36. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +138 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +26 -0
  38. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +227 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +83 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +90 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +274 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +192 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +36 -0
  44. package/package.json +1 -1
@@ -0,0 +1,471 @@
1
+ "use strict";
2
+ /**
3
+ * Live Postgres coverage for the queue-local CLUSTER lease path — the
4
+ * dialect-aware enqueue + the transactional FOR UPDATE SKIP LOCKED claim that
5
+ * only runs when the queue's store is a postgres/mysql cluster backend. Host
6
+ * smoke exercises ONLY the sqlite single-statement RETURNING path; this proves
7
+ * the Postgres branch of queue-local.lease() against a real server:
8
+ *
9
+ * - the head-of-queue rows are SELECT … FOR UPDATE SKIP LOCKED inside a
10
+ * transaction (so concurrent leasers see disjoint sets — no double-lease
11
+ * from the frozen-materialized-subquery the single-statement form hits on
12
+ * Postgres), then
13
+ * - the guarded UPDATE … WHERE status='pending' AND "_id" IN (…) RETURNING
14
+ * hands the claimed rows back in one round trip (Postgres supports
15
+ * RETURNING; this is the branch MySQL can't take).
16
+ *
17
+ * Driven through the REAL queue-local consumer surface (the backend factory
18
+ * b.queue.init wires): enqueue / lease / complete / fail / sweepExpired /
19
+ * dlqList against real Postgres. RED before the dialect reshape (the
20
+ * single-statement RETURNING UPDATE freezes its subquery qual on PG, so
21
+ * concurrent leasers double-claim); GREEN after.
22
+ *
23
+ * The "driver" is the persistent-session docker-exec psql shim shared in
24
+ * spirit with data-layer-cluster-pg — a held psql process per client, so a
25
+ * transaction's BEGIN / body / COMMIT and the FOR UPDATE locks run on ONE
26
+ * session (faithful to node-postgres). BIGINT comes back as a JS STRING
27
+ * (node-pg's int8 default); _shapeLeasedRow's Number() coercion is what
28
+ * normalizes the framework int columns.
29
+ *
30
+ * RUN: node scripts/test-integration.js --skip-service-check queue-cluster-pg
31
+ */
32
+
33
+ var spawn = require("node:child_process").spawn;
34
+ var execFileSync = require("node:child_process").execFileSync;
35
+ var fs = require("node:fs");
36
+ var os = require("node:os");
37
+ var path = require("node:path");
38
+ var helpers = require("../helpers");
39
+ var check = helpers.check;
40
+ var services = require("../helpers/services");
41
+ var b = require("../../");
42
+ var queueLocal = require("../../lib/queue-local");
43
+
44
+ var CONTAINER = "blamejs-test-postgres";
45
+ var NULL_SENTINEL = "__BJNULL__";
46
+ var PSQL_ARGS = "psql -U blamejs -d blamejs_test -A " +
47
+ "-v ON_ERROR_STOP=0 -P null=__BJNULL__ 2>&1";
48
+
49
+ // ---- one-shot psql (setup / teardown / out-of-band assertions) ----
50
+ function _psql(sql) {
51
+ var prelude = "\\pset fieldsep '\\t'\n";
52
+ var out = execFileSync(
53
+ "docker",
54
+ ["exec", "-i", CONTAINER, "sh", "-c",
55
+ "psql -U blamejs -d blamejs_test -qtA -P null=__BJNULL__ 2>&1"],
56
+ { input: prelude + sql + "\n", stdio: ["pipe", "pipe", "pipe"] }
57
+ ).toString("utf8");
58
+ if (/^ERROR:/m.test(out)) {
59
+ throw new Error("psql setup failed for [" + sql + "]:\n" + out);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ // ---- persistent-session docker-exec psql driver (faithful to node-pg) ----
65
+ var _seq = 0;
66
+ function _makeDockerPgDriver() {
67
+ return {
68
+ connect: function () {
69
+ return new Promise(function (resolve, reject) {
70
+ var child = spawn(
71
+ "docker",
72
+ ["exec", "-i", CONTAINER, "sh", "-c",
73
+ PSQL_ARGS + " ; echo __BLAMEJS_PSQL_EXIT__"],
74
+ { stdio: ["pipe", "pipe", "pipe"] }
75
+ );
76
+ var client = { child: child, buf: "", pending: null, closed: false };
77
+ child.on("error", function (e) {
78
+ if (client.pending) { var p = client.pending; client.pending = null; p.reject(e); }
79
+ });
80
+ child.on("close", function () {
81
+ client.closed = true;
82
+ if (client.pending) {
83
+ var p = client.pending; client.pending = null;
84
+ p.reject(new Error("psql session closed mid-statement"));
85
+ }
86
+ });
87
+ child.stdout.on("data", function (chunk) {
88
+ client.buf += chunk.toString("utf8");
89
+ _drain(client);
90
+ });
91
+ var primeSentinel = "__BJ_PRIME__";
92
+ client.pending = {
93
+ sentinel: primeSentinel,
94
+ resolve: function () { resolve(client); },
95
+ reject: reject,
96
+ };
97
+ client.child.stdin.write(
98
+ "\\pset fieldsep '\\t'\n\\pset footer off\n\\set VERBOSITY verbose\n" +
99
+ "\\echo " + primeSentinel + "\n");
100
+ });
101
+ },
102
+
103
+ query: function (client, sql, params) {
104
+ params = params || [];
105
+ var bound = _bindParams(sql, params);
106
+ var sentinel = "__BJ_EOR_" + (++_seq) + "__";
107
+ return new Promise(function (resolve, reject) {
108
+ if (client.closed) { reject(new Error("psql session is closed")); return; }
109
+ client.pending = { sentinel: sentinel, resolve: resolve, reject: reject };
110
+ client.child.stdin.write(bound + "\n;\n\\echo " + sentinel + "\n");
111
+ });
112
+ },
113
+
114
+ close: function (client) {
115
+ return new Promise(function (resolve) {
116
+ if (client.closed) { resolve(); return; }
117
+ try { client.child.stdin.end("\\q\n"); } catch (_e) { /* best effort */ }
118
+ var done = false;
119
+ client.child.on("close", function () { if (!done) { done = true; resolve(); } });
120
+ setTimeout(function () {
121
+ if (done) return;
122
+ done = true;
123
+ try { client.child.kill("SIGKILL"); } catch (_e) {}
124
+ resolve();
125
+ }, 2000);
126
+ });
127
+ },
128
+
129
+ dialect: "postgres",
130
+ };
131
+ }
132
+
133
+ function _drain(client) {
134
+ if (!client.pending) return;
135
+ var sentinel = client.pending.sentinel;
136
+ var marker = "\n" + sentinel + "\n";
137
+ var idx = client.buf.indexOf(marker);
138
+ var startAtZero = client.buf.indexOf(sentinel + "\n") === 0;
139
+ var block;
140
+ if (idx !== -1) {
141
+ block = client.buf.slice(0, idx);
142
+ client.buf = client.buf.slice(idx + marker.length);
143
+ } else if (startAtZero) {
144
+ block = "";
145
+ client.buf = client.buf.slice((sentinel + "\n").length);
146
+ } else {
147
+ return;
148
+ }
149
+ var p = client.pending;
150
+ client.pending = null;
151
+ var parsed;
152
+ try { parsed = _parseBlock(block); }
153
+ catch (e) { return p.reject(e); }
154
+ if (parsed.error) return p.reject(parsed.error);
155
+ p.resolve({ rows: parsed.rows, rowCount: parsed.rowCount });
156
+ }
157
+
158
+ function _bindParams(sql, params) {
159
+ return sql.replace(/\$(\d+)/g, function (_m, n) {
160
+ var i = Number(n) - 1;
161
+ if (i < 0 || i >= params.length) {
162
+ throw new Error("placeholder $" + n + " has no matching param");
163
+ }
164
+ var v = params[i];
165
+ if (v === null || v === undefined) return "NULL";
166
+ // node-postgres serializes a JS array param for `"_id" = ANY($n)` into a
167
+ // Postgres array on the wire; the docker-exec shim reproduces that as an
168
+ // ARRAY[…] literal so the queue's array-param lease UPDATE is exercised
169
+ // faithfully (a plain String(arr) would comma-join → "malformed array
170
+ // literal"). The lease returns early on an empty id set, so ARRAY[] never
171
+ // actually reaches here, but render it typed for completeness.
172
+ if (Array.isArray(v)) {
173
+ if (v.length === 0) return "'{}'";
174
+ return "ARRAY[" + v.map(function (el) {
175
+ if (el === null || el === undefined) return "NULL";
176
+ if (typeof el === "number") return String(el);
177
+ if (typeof el === "boolean") return el ? "TRUE" : "FALSE";
178
+ return "'" + String(el).replace(/'/g, "''") + "'";
179
+ }).join(",") + "]";
180
+ }
181
+ if (typeof v === "number") return String(v);
182
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
183
+ return "'" + String(v).replace(/'/g, "''") + "'";
184
+ });
185
+ }
186
+
187
+ var _CMD_TAG_RE = /^(INSERT|UPDATE|DELETE|MERGE|SELECT|COPY|MOVE)\b(?:\s+\d+)*\s*$/;
188
+ var _CTRL_TAG_RE = /^(BEGIN|COMMIT|ROLLBACK|SET|RESET|SAVEPOINT|RELEASE|START|CREATE|DROP|ALTER|GRANT|REVOKE|TRUNCATE|COMMENT|DO|CALL|VACUUM|ANALYZE|EXPLAIN|TABLE|SHOW|DISCARD)\b/;
189
+
190
+ function _parseBlock(block) {
191
+ var lines = block.split(/\r?\n/);
192
+ while (lines.length && lines[lines.length - 1] === "") lines.pop();
193
+
194
+ for (var i = 0; i < lines.length; i++) {
195
+ var em = /^ERROR:\s+([0-9A-Za-z]{5}):\s*(.*)$/.exec(lines[i]);
196
+ if (em) {
197
+ var err = new Error("Postgres " + em[1] + ": " + em[2]);
198
+ err.code = em[1];
199
+ return { error: err };
200
+ }
201
+ }
202
+
203
+ var affected = null;
204
+ var dataLines = [];
205
+ for (var j = 0; j < lines.length; j++) {
206
+ var ln = lines[j];
207
+ if (/^(NOTICE|WARNING|DETAIL|HINT|LINE|LOCATION|CONTEXT|STATEMENT):/.test(ln)) continue;
208
+ var tm = _CMD_TAG_RE.exec(ln);
209
+ if (tm) {
210
+ var nums = ln.trim().split(/\s+/).slice(1).map(Number);
211
+ if (nums.length) affected = nums[nums.length - 1];
212
+ continue;
213
+ }
214
+ if (_CTRL_TAG_RE.test(ln) && ln.indexOf("\t") === -1) continue;
215
+ dataLines.push(ln);
216
+ }
217
+
218
+ var rows = [];
219
+ if (dataLines.length >= 1) {
220
+ var headers = dataLines[0].split("\t");
221
+ for (var k = 1; k < dataLines.length; k++) {
222
+ var cells = dataLines[k].split("\t");
223
+ var row = {};
224
+ for (var c = 0; c < headers.length; c++) {
225
+ var cell = cells[c];
226
+ row[headers[c]] = (cell === NULL_SENTINEL || cell === undefined) ? null : cell;
227
+ }
228
+ rows.push(row);
229
+ }
230
+ }
231
+ var rowCount = (affected !== null) ? affected : rows.length;
232
+ return { rows: rows, rowCount: rowCount, error: null };
233
+ }
234
+
235
+ var OWNED_TABLES = [
236
+ "_blamejs_jobs",
237
+ "_blamejs_cluster_state",
238
+ "_blamejs_leader",
239
+ "_blamejs_audit_tip",
240
+ "_blamejs_consent_tip",
241
+ ];
242
+
243
+ function _dropOwned() {
244
+ _psql(OWNED_TABLES.map(function (t) {
245
+ return "DROP TABLE IF EXISTS " + t + " CASCADE;";
246
+ }).join("\n"));
247
+ }
248
+
249
+ // Out-of-band single-column read for one job row (double-quoted camelCase).
250
+ function _jobCol(jobId, col) {
251
+ var out = _psql("SELECT \"" + col + "\" FROM \"_blamejs_jobs\" WHERE \"_id\" = '" +
252
+ jobId.replace(/'/g, "''") + "';");
253
+ var v = out.split(/\r?\n/).filter(function (l) { return l.length > 0; })[0];
254
+ return (v === undefined || v === NULL_SENTINEL) ? null : v;
255
+ }
256
+
257
+ function _countPg(whereClause) {
258
+ var out = _psql("SELECT count(*) FROM \"_blamejs_jobs\"" +
259
+ (whereClause ? " WHERE " + whereClause : "") + ";");
260
+ var v = out.split(/\r?\n/).filter(function (l) { return l.length > 0; })[0];
261
+ return Number(v);
262
+ }
263
+
264
+ function _createJobsTable() {
265
+ _psql("DROP TABLE IF EXISTS \"_blamejs_jobs\" CASCADE;");
266
+ _psql(
267
+ "CREATE TABLE \"_blamejs_jobs\" (" +
268
+ " \"_id\" VARCHAR(64) PRIMARY KEY," +
269
+ " \"queueName\" VARCHAR(255) NOT NULL," +
270
+ " \"payload\" TEXT," +
271
+ " \"status\" VARCHAR(32) NOT NULL," +
272
+ " \"enqueuedAt\" BIGINT NOT NULL," +
273
+ " \"availableAt\" BIGINT NOT NULL," +
274
+ " \"leasedAt\" BIGINT," +
275
+ " \"leaseExpiresAt\" BIGINT," +
276
+ " \"attempts\" BIGINT NOT NULL DEFAULT 0," +
277
+ " \"maxAttempts\" BIGINT NOT NULL DEFAULT 5," +
278
+ " \"lastError\" TEXT," +
279
+ " \"finishedAt\" BIGINT," +
280
+ " \"traceId\" VARCHAR(255)," +
281
+ " \"classification\" VARCHAR(255)," +
282
+ " \"priority\" BIGINT NOT NULL DEFAULT 0," +
283
+ " \"repeatCron\" VARCHAR(255)," +
284
+ " \"repeatTimezone\" VARCHAR(255)," +
285
+ " \"flowId\" VARCHAR(255)," +
286
+ " \"flowChildName\" VARCHAR(255)," +
287
+ " \"dependsOn\" TEXT" +
288
+ ");");
289
+ _psql("CREATE INDEX \"_blamejs_jobs_lease_idx\" ON \"_blamejs_jobs\" " +
290
+ "(\"queueName\", \"status\", \"availableAt\");");
291
+ }
292
+
293
+ async function run() {
294
+ var pg = await services.requireService("postgres");
295
+ if (!pg.ok) throw new Error("postgres unreachable: " + pg.reason);
296
+
297
+ _dropOwned();
298
+
299
+ var driver = _makeDockerPgDriver();
300
+ b.cluster._resetForTest();
301
+ b.externalDb._resetForTest();
302
+ b.db._resetForTest();
303
+ b.externalDb.init({
304
+ backends: {
305
+ ops: {
306
+ connect: driver.connect, query: driver.query, close: driver.close,
307
+ dialect: "postgres",
308
+ },
309
+ },
310
+ });
311
+
312
+ var vaultDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-q-cl-pg-"));
313
+ await helpers.setupVaultOnly(vaultDir);
314
+ queueLocal._ensureSealTable();
315
+
316
+ _createJobsTable();
317
+
318
+ await b.cluster.init({
319
+ nodeId: "queue-node-pg",
320
+ role: "leader",
321
+ leaseTtl: b.constants.TIME.seconds(60),
322
+ heartbeatInterval: b.constants.TIME.seconds(20),
323
+ externalDbBackend: "ops",
324
+ dialect: "postgres",
325
+ });
326
+
327
+ try {
328
+ check("queue-cluster (pg): cluster mode routes the queue store to Postgres",
329
+ b.cluster.isClusterMode() === true && b.clusterStorage.dialect() === "postgres");
330
+
331
+ var q = queueLocal.create({});
332
+
333
+ await _proveEnqueueAndClusterLease(q);
334
+ await _proveComplete(q);
335
+ await _proveFailRetryThenFinalDlq(q);
336
+ await _proveSweepExpired(q);
337
+ await _proveDelayedNotLeased(q);
338
+ } finally {
339
+ try { await b.cluster.shutdown(); } catch (_e) {}
340
+ b.cluster._resetForTest();
341
+ try { await b.externalDb.shutdown(); } catch (_e) {}
342
+ b.externalDb._resetForTest();
343
+ try { helpers.teardownVaultOnly(vaultDir); } catch (_e) {}
344
+ _dropOwned();
345
+ }
346
+ }
347
+
348
+ // ======================================================================
349
+ // 1. enqueue (dialect INSERT) + the transactional cluster lease via the
350
+ // Postgres RETURNING branch — the #35 headline on PG.
351
+ // ======================================================================
352
+ async function _proveEnqueueAndClusterLease(q) {
353
+ var Q = "q-claim";
354
+ await q.enqueue(Q, { job: "low" }, { priority: 0 });
355
+ await q.enqueue(Q, { job: "high" }, { priority: 10 });
356
+ await q.enqueue(Q, { job: "mid" }, { priority: 5 });
357
+
358
+ check("enqueue (pg): three INSERTs landed (double-quoted camelCase identifiers)",
359
+ _countPg("\"queueName\" = 'q-claim'") === 3);
360
+ check("enqueue (pg): all three start pending",
361
+ _countPg("\"queueName\" = 'q-claim' AND \"status\" = 'pending'") === 3);
362
+ check("enqueue (pg): the sealed payload is NOT stored in cleartext",
363
+ _countPg("\"queueName\" = 'q-claim' AND \"payload\" LIKE '%\"job\"%'") === 0);
364
+
365
+ var leased = await q.lease(Q, b.constants.TIME.seconds(30), 2);
366
+ check("cluster-lease (pg): leased exactly 2 via FOR UPDATE SKIP LOCKED + RETURNING",
367
+ Array.isArray(leased) && leased.length === 2);
368
+ // ORDER BY priority desc governs WHICH rows are claimed, not the RETURNING
369
+ // array order (Postgres does not guarantee RETURNING follows the qual's
370
+ // ORDER BY). Assert the claimed SET + that the low-priority job is left.
371
+ var leasedJobs = leased.map(function (l) { return l.payload && l.payload.job; });
372
+ check("cluster-lease (pg): the two HIGHEST-priority jobs were claimed (high + mid)",
373
+ leasedJobs.indexOf("high") !== -1 && leasedJobs.indexOf("mid") !== -1 &&
374
+ leasedJobs.indexOf("low") === -1);
375
+ check("cluster-lease (pg): RETURNING handed back rows with attempts coerced to 1",
376
+ leased[0].attempts === 1 && leased[1].attempts === 1);
377
+ check("cluster-lease (pg): the two leased rows flipped to inflight on the server",
378
+ _countPg("\"queueName\" = 'q-claim' AND \"status\" = 'inflight'") === 2);
379
+ check("cluster-lease (pg): the un-leased low-priority job stays pending",
380
+ _countPg("\"queueName\" = 'q-claim' AND \"status\" = 'pending'") === 1);
381
+ check("cluster-lease (pg): server-side attempts column incremented to 1",
382
+ Number(_jobCol(leased[0].jobId, "attempts")) === 1);
383
+
384
+ _proveEnqueueAndClusterLease._leased = leased;
385
+ }
386
+
387
+ // ======================================================================
388
+ // 2. complete()
389
+ // ======================================================================
390
+ async function _proveComplete(q) {
391
+ var high = _proveEnqueueAndClusterLease._leased[0];
392
+ var ok = await q.complete(high.jobId);
393
+ check("complete (pg): inflight→done flip returned true", ok === true);
394
+ check("complete (pg): status is done with finishedAt set on the server",
395
+ _jobCol(high.jobId, "status") === "done" && _jobCol(high.jobId, "finishedAt") !== null);
396
+
397
+ var again = await q.complete(high.jobId);
398
+ check("complete (pg): a second complete() on the done row no-ops (status guard)",
399
+ again === false);
400
+ }
401
+
402
+ // ======================================================================
403
+ // 3. fail() — retry CASE branch + final-failure → DLQ.
404
+ // ======================================================================
405
+ async function _proveFailRetryThenFinalDlq(q) {
406
+ var mid = _proveEnqueueAndClusterLease._leased[1];
407
+ var retried = await q.fail(mid.jobId, "transient blip", { retryDelayMs: 0 });
408
+ check("fail-retry (pg): inflight→pending retry returned true", retried === true);
409
+ check("fail-retry (pg): retried job is pending again (attempts 1 < max 5)",
410
+ _jobCol(mid.jobId, "status") === "pending");
411
+ check("fail-retry (pg): lastError is sealed, not cleartext on the server",
412
+ _jobCol(mid.jobId, "lastError") !== null &&
413
+ !/transient blip/.test(String(_jobCol(mid.jobId, "lastError"))));
414
+
415
+ var released = await q.lease("q-claim", b.constants.TIME.seconds(30), 5);
416
+ check("fail-retry (pg): the retried job re-enters the lease set",
417
+ released.length === 2);
418
+
419
+ var FQ = "q-final";
420
+ await q.enqueue(FQ, { job: "doomed" }, { maxAttempts: 1 });
421
+ var fl = await q.lease(FQ, b.constants.TIME.seconds(30), 1);
422
+ check("fail-final (pg): the doomed job leased (attempts→1 == maxAttempts)",
423
+ fl.length === 1 && fl[0].attempts === 1);
424
+ var finalFail = await q.fail(fl[0].jobId, "fatal error", { retryDelayMs: 0 });
425
+ check("fail-final (pg): the exhausted job's fail returned true", finalFail === true);
426
+ check("fail-final (pg): exhausted job moved to failed (DLQ) on the server",
427
+ _jobCol(fl[0].jobId, "status") === "failed" && _jobCol(fl[0].jobId, "finishedAt") !== null);
428
+ check("fail-final (pg): dlqSize counts the failed job",
429
+ (await q.dlqSize(FQ)) === 1);
430
+ var dlq = await q.dlqList(FQ);
431
+ check("fail-final (pg): dlqList unseals lastError back to cleartext",
432
+ dlq.length === 1 && dlq[0].lastError === "fatal error" &&
433
+ dlq[0].payload && dlq[0].payload.job === "doomed");
434
+ }
435
+
436
+ // ======================================================================
437
+ // 4. sweepExpired()
438
+ // ======================================================================
439
+ async function _proveSweepExpired(q) {
440
+ var SQ = "q-sweep";
441
+ await q.enqueue(SQ, { job: "sweepable" });
442
+ var leased = await q.lease(SQ, 1, 1);
443
+ check("sweep (pg): job leased before sweep", leased.length === 1);
444
+ await helpers.passiveObserve(30, "queue-cluster (pg): lease ms window elapses before sweep");
445
+ var swept = await q.sweepExpired();
446
+ check("sweep (pg): sweepExpired re-queued at least the expired lease", swept >= 1);
447
+ check("sweep (pg): the swept job is pending again with leaseExpiresAt cleared",
448
+ _jobCol(leased[0].jobId, "status") === "pending" &&
449
+ _jobCol(leased[0].jobId, "leaseExpiresAt") === null);
450
+ }
451
+
452
+ // ======================================================================
453
+ // 5. a future-availableAt job is not leasable but still counts in size().
454
+ // ======================================================================
455
+ async function _proveDelayedNotLeased(q) {
456
+ var DQ = "q-delay";
457
+ await q.enqueue(DQ, { job: "later" }, { availableAt: Date.now() + b.constants.TIME.minutes(5) });
458
+ var leased = await q.lease(DQ, b.constants.TIME.seconds(30), 5);
459
+ check("delay (pg): a future-availableAt job is not leased", leased.length === 0);
460
+ check("delay (pg): the future job still counts toward size()",
461
+ (await q.size(DQ)) === 1);
462
+ }
463
+
464
+ module.exports = { run: run };
465
+
466
+ if (require.main === module) {
467
+ run().then(
468
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
469
+ function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
470
+ );
471
+ }