Ctrl K
ComunidadGuías…

test/jobs-dispatch-kick.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { config, sessions } from '../server/state.js';
import { REQUEUE_HOLD_MS } from '../lib/jobs.js';
import { addJob, moveJob, boardSettings, allJobs, startDispatcher, stopDispatcher, DISPATCH_DEBOUNCE_MS } from '../server/jobs.js';

// A card posted, requeued or let through by a freed slot goes out within the
// debounce window, not on the next 5-minute scan.
const REPO = mkdtempSync(join(tmpdir(), 'a007-kickrepo-'));
const noop = () => {};

function fakeCreateSession(calls, { gate } = {}) {
  let n = 0;
  return async (command, name, repoPath, branch, ownerId, meta) => {
    calls.push(meta.jobId);
    if (gate) await gate;
    n++;
    const session = { id: `s${n}`, name: `Agent${n}`, branchName: `b${n}`, worktreePath: `/wt/${n}`, state: 'WORKING', exited: false, jobId: meta.jobId };
    sessions.set(session.id, session);
    return { session };
  };
}

const post = (title) => addJob({ title, repoPath: REPO }, noop).job;

async function start(createSession, { running = true, maxPerRepo = 3 } = {}) {
  config.repos = [{ path: REPO }];
  config.jobs = [];
  config.jobBoard = null;
  sessions.clear();
  boardSettings().maxPerRepo = maxPerRepo;
  startDispatcher(createSession, noop);
  // Let the start-up tick go by on a stopped board, so the scan (and its
  // GitHub sweeps) stays out of these tests for the next interval.
  await vi.advanceTimersByTimeAsync(2500);
  boardSettings().running = running;
}

describe('dispatch on events', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { stopDispatcher(); vi.useRealTimers(); });

  it('dispatches a card posted with room within the debounce window', async () => {
    const calls = [];
    await start(fakeCreateSession(calls));
    const job = post('Now please');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS - 100);
    expect(calls).toEqual([]);
    await vi.advanceTimersByTimeAsync(200);
    expect(calls).toEqual([job.id]);
    expect(job.state).toBe('in-progress');
  });

  it('leaves a card queued when its repo is at the cap, and sends it when a slot frees', async () => {
    const calls = [];
    await start(fakeCreateSession(calls), { maxPerRepo: 1 });
    const first = post('First');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    const second = post('Second');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    expect(calls).toEqual([first.id]);
    expect(second.state).toBe('todo');

    await moveJob(first.id, 'review', noop, { findPr: async () => ({ pr: null }) });
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    expect(calls).toEqual([first.id, second.id]);
    expect(second.state).toBe('in-progress');
  });

  it('dispatches a card moved back to To do once its hold lifts', async () => {
    const calls = [];
    await start(fakeCreateSession(calls));
    const job = post('Again');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    await moveJob(job.id, 'todo', noop);
    expect(job.state).toBe('todo');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    expect(calls).toEqual([job.id]);   // held, so it can be edited first
    await vi.advanceTimersByTimeAsync(REQUEUE_HOLD_MS);
    expect(calls).toEqual([job.id, job.id]);
    expect(job.state).toBe('in-progress');
  });

  it('coalesces a burst of posts into one pass', async () => {
    const calls = [];
    await start(fakeCreateSession(calls));
    const a = post('A');
    await vi.advanceTimersByTimeAsync(800);
    const b = post('B');
    await vi.advanceTimersByTimeAsync(800);
    const c = post('C');
    // One timer, armed by the first post: all three go on it.
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS - 1600 + 50);
    expect(calls).toEqual([a.id, b.id, c.id]);
    await vi.advanceTimersByTimeAsync(10_000);
    expect(calls).toHaveLength(3);
  });

  it('does nothing while the board is stopped', async () => {
    const calls = [];
    await start(fakeCreateSession(calls), { running: false });
    const job = post('Held');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS * 3);
    expect(calls).toEqual([]);
    expect(job.state).toBe('todo');
  });

  it('never runs two passes at once, and retries once the first is done', async () => {
    const calls = [];
    let release;
    const gate = new Promise(r => { release = r; });
    await start(fakeCreateSession(calls, { gate }));
    const a = post('A');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    expect(calls).toEqual([a.id]);   // held inside createSession
    const b = post('B');
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS * 3);
    expect(calls).toEqual([a.id]);   // no second pass while the first runs
    release();
    await vi.advanceTimersByTimeAsync(DISPATCH_DEBOUNCE_MS + 100);
    expect(calls).toEqual([a.id, b.id]);
    expect(allJobs().map(j => j.state)).toEqual(['in-progress', 'in-progress']);
  });
});