jobQueue.test.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { after, before, beforeEach, test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { prepareTestDatabase } from '../helpers/testServer.js';
  4. import { applyTestConfig } from '../helpers/testConfig.js';
  5. const dbUrl = prepareTestDatabase('test-jobqueue');
  6. const { enqueue, claimNextJob, completeJob, failJob, queueStats } = await import(
  7. '../../src/queue/jobQueue.js'
  8. );
  9. const { db, disconnectDb } = await import('../../src/store/db.js');
  10. before(() => applyTestConfig({ DATABASE_URL: dbUrl, WORKER_MAX_ATTEMPTS: '2' }));
  11. beforeEach(async () => {
  12. await db().job.deleteMany({});
  13. });
  14. after(async () => {
  15. await disconnectDb();
  16. });
  17. test('an enqueued job can be claimed exactly once', async () => {
  18. await enqueue({ type: 'chatwoot_message', payload: { conversationId: 1 } });
  19. const first = await claimNextJob();
  20. assert.ok(first);
  21. assert.equal(first.status, 'processing');
  22. assert.equal(first.attempts, 1);
  23. const second = await claimNextJob();
  24. assert.equal(second, null, 'a claimed job must not be handed out again');
  25. });
  26. test('completing a job clears the error and marks it done', async () => {
  27. const id = await enqueue({ type: 'chatwoot_message', payload: {} });
  28. await claimNextJob();
  29. await completeJob(id);
  30. const job = await db().job.findUnique({ where: { id } });
  31. assert.equal(job?.status, 'done');
  32. assert.equal(job?.lastError, null);
  33. assert.ok(job?.finishedAt);
  34. });
  35. test('a failure below the attempt limit is re-queued with a future runAfter', async () => {
  36. const id = await enqueue({ type: 'chatwoot_message', payload: {} });
  37. await claimNextJob();
  38. const disposition = await failJob(id, 'Flowise returned HTTP 502');
  39. assert.equal(disposition, 'retry');
  40. const job = await db().job.findUnique({ where: { id } });
  41. assert.equal(job?.status, 'queued');
  42. assert.ok(job && job.runAfter.getTime() > Date.now(), 'backoff must delay the retry');
  43. assert.match(job?.lastError ?? '', /502/);
  44. const claimed = await claimNextJob();
  45. assert.equal(claimed, null, 'a backed-off job is not runnable yet');
  46. });
  47. test('exhausting the attempt limit parks the job as dead', async () => {
  48. const id = await enqueue({ type: 'chatwoot_message', payload: {} });
  49. await claimNextJob();
  50. assert.equal(await failJob(id, 'boom 1'), 'retry');
  51. await db().job.update({ where: { id }, data: { runAfter: new Date(0) } });
  52. await claimNextJob();
  53. assert.equal(await failJob(id, 'boom 2'), 'dead');
  54. const job = await db().job.findUnique({ where: { id } });
  55. assert.equal(job?.status, 'dead');
  56. assert.equal(job?.attempts, 2);
  57. });
  58. test('secrets never reach the stored payload or the stored error', async () => {
  59. const id = await enqueue({
  60. type: 'chatwoot_message',
  61. payload: { api_token: 'super-secret-token', conversationId: 5 },
  62. });
  63. await claimNextJob();
  64. await failJob(id, 'failed calling https://shop.test/x?consumer_key=ck_leakedvalue');
  65. const job = await db().job.findUnique({ where: { id } });
  66. assert.ok(!job?.payloadJson.includes('super-secret-token'));
  67. assert.ok(!job?.lastError?.includes('ck_leakedvalue'));
  68. });
  69. test('queueStats counts jobs per status', async () => {
  70. await enqueue({ type: 'chatwoot_message', payload: {} });
  71. await enqueue({ type: 'chatwoot_message', payload: {} });
  72. const id = await claimNextJob();
  73. assert.ok(id);
  74. await completeJob(id.id);
  75. const stats = await queueStats();
  76. assert.equal(stats.queued, 1);
  77. assert.equal(stats.done, 1);
  78. });