| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- /**
- * Sidekiq job-dispatch bridge (Ruby).
- *
- * Sidekiq decouples a job enqueue from the worker's `perform`, linked by the WORKER CLASS
- * NAME: `DestroyUserWorker.perform_async(id)` has no static edge to `DestroyUserWorker#perform`
- * (usually a different file). This bridges each `Worker.perform_async`/`.perform_in`/`.perform_at`
- * site to that worker's instance `perform`, gated on the class including `Sidekiq::Job`/`Worker`.
- * Covers both include aliases, the scheduled forms, namespace disambiguation (two `NotifyWorker`s
- * in different modules resolve to the right one by qualified name), and the precision boundary: a
- * non-worker class with a `perform`, and an ActiveJob `perform_later`, both produce no edge.
- */
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
- import * as fs from 'node:fs';
- import * as path from 'node:path';
- import * as os from 'node:os';
- import { CodeGraph } from '../src';
- describe('sidekiq-dispatch synthesizer', () => {
- let dir: string;
- beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidekiq-dispatch-')); });
- afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
- const write = (rel: string, body: string) => {
- const p = path.join(dir, rel);
- fs.mkdirSync(path.dirname(p), { recursive: true });
- fs.writeFileSync(p, body);
- };
- it('bridges perform_async/_in to #perform, disambiguates namespaces, ignores non-workers and ActiveJob', async () => {
- write('app/workers/destroy_user_worker.rb', `class DestroyUserWorker
- include Sidekiq::Worker
- def perform(user_id)
- User.find(user_id).destroy!
- end
- end
- `);
- // Modern Sidekiq::Job alias + the scheduled form.
- write('app/workers/send_email_worker.rb', `class SendEmailWorker
- include Sidekiq::Job
- def perform(addr)
- end
- end
- `);
- // Namespace collision: two NotifyWorkers, same simple name, different modules.
- write('app/workers/comments/notify_worker.rb', `module Comments
- class NotifyWorker
- include Sidekiq::Job
- def perform(id)
- end
- end
- end
- `);
- write('app/workers/articles/notify_worker.rb', `module Articles
- class NotifyWorker
- include Sidekiq::Job
- def perform(id)
- end
- end
- end
- `);
- // A non-worker class that happens to have a `perform` method — never a target.
- write('app/services/report.rb', `class Report
- def perform(x)
- end
- end
- `);
- // An ActiveJob — dispatched via perform_later, a different shape, not matched.
- write('app/jobs/cleanup_job.rb', `class CleanupJob < ApplicationJob
- def perform
- end
- end
- `);
- write('app/services/user_service.rb', `class UserService
- def deactivate(user)
- DestroyUserWorker.perform_async(user.id)
- SendEmailWorker.perform_in(5, user.email)
- Comments::NotifyWorker.perform_async(1)
- Articles::NotifyWorker.perform_async(2)
- Report.perform_async(3)
- CleanupJob.perform_later
- end
- end
- `);
- const cg = await CodeGraph.init(dir, { silent: true });
- await cg.indexAll();
- const db = (cg as any).db.db;
- const edges = db
- .prepare(
- `SELECT s.name source, t.name target, t.file_path tf, json_extract(e.metadata,'$.via') via
- FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
- WHERE json_extract(e.metadata,'$.synthesizedBy') = 'sidekiq-dispatch'`
- )
- .all();
- // Four enqueues bridge: both aliases, perform_async + perform_in, two namespaced.
- expect(edges.map((r: any) => r.via).sort()).toEqual([
- 'Articles::NotifyWorker', 'Comments::NotifyWorker', 'DestroyUserWorker', 'SendEmailWorker',
- ]);
- expect(edges.every((r: any) => r.target === 'perform' && r.source === 'deactivate')).toBe(true);
- // Namespace disambiguation: each NotifyWorker hits its OWN module's file, not the other.
- expect(edges.find((r: any) => r.via === 'Comments::NotifyWorker').tf).toMatch(/comments[\\/]notify_worker\.rb$/);
- expect(edges.find((r: any) => r.via === 'Articles::NotifyWorker').tf).toMatch(/articles[\\/]notify_worker\.rb$/);
- // PRECISION: a non-worker `perform`, and ActiveJob `perform_later`, contribute nothing.
- expect(edges.some((r: any) => r.via === 'Report')).toBe(false);
- expect(edges.some((r: any) => /Cleanup/.test(r.via))).toBe(false);
- cg.close?.();
- });
- it('produces no edges in a Ruby project with no Sidekiq (clean control)', async () => {
- write('lib/calc.rb', `class Calc
- def add(a, b)
- a + b
- end
- end
- `);
- const cg = await CodeGraph.init(dir, { silent: true });
- await cg.indexAll();
- const db = (cg as any).db.db;
- const count = db
- .prepare(`SELECT count(*) c FROM edges WHERE json_extract(metadata,'$.synthesizedBy') = 'sidekiq-dispatch'`)
- .get();
- expect(count.c).toBe(0);
- cg.close?.();
- });
- });
|