/**
 * Fleet coordinator — the concurrency core. Memory provider; deterministic
 * (race driven via the rev compare-and-swap, not real threads).
 */

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
import { ConflictError, BadRequestError } from '@bytelyst/errors';
import { _resetDatastoreProvider, setProvider } from '../../lib/datastore.js';
import * as repo from './repository.js';
import * as coord from './coordinator.js';
import type { SubmitJobInput } from './types.js';

const PID = 'lysnrai';

function input(over: Partial<SubmitJobInput> = {}): SubmitJobInput {
  return {
    idempotencyKey: 'task-1',
    bodyMd: '# do the thing',
    priority: 'medium',
    capabilities: [],
    prefersEngine: [],
    allowedScope: [],
    deps: [],
    kind: 'leaf',
    ...over,
  };
}

const factory = (over = {}) => ({
  productId: PID,
  factoryId: 'fac_1',
  capabilities: [] as string[],
  leaseSeconds: 900,
  ...over,
});

describe('fleet coordinator', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('submit: new job is queued with no deps', async () => {
    const { job, outcome } = await coord.submitJob(PID, input());
    expect(outcome).toBe('created');
    expect(job.stage).toBe('queued');
    expect(job.productId).toBe(PID);
  });

  it('submit with an explicit engine stores it and the run carries it at claim', async () => {
    const { job } = await coord.submitJob(PID, input({ engine: 'claude' }));
    expect(job.engine).toBe('claude');
    const claim = await coord.claimNextJob(factory());
    expect(claim?.run.engine).toBe('claude');
    // engineless job falls back (no concrete engine forced onto the run)
    const { job: j2 } = await coord.submitJob(PID, input({ idempotencyKey: 'task-2' }));
    expect(j2.engine).toBeUndefined();
  });

  // ── ATOMIC CLAIM RACE (TRUE concurrency, not sequential) ──
  it('atomic claim: two TRULY concurrent contenders on the same job version — exactly one wins', async () => {
    const { job } = await coord.submitJob(PID, input());

    // Both contenders see the SAME freshly-read job version and race via Promise.all.
    // The datastore compare-and-set (updateIfMatch) admits exactly one winner.
    const [a, b] = await Promise.all([
      coord.tryClaimJob(job, factory({ factoryId: 'fac_A' })),
      coord.tryClaimJob(job, factory({ factoryId: 'fac_B' })),
    ]);

    const oks = [a, b].filter(r => r.ok);
    const conflicts = [a, b].filter(r => !r.ok);
    expect(oks).toHaveLength(1);
    expect(conflicts).toHaveLength(1);
    expect(conflicts[0].ok === false && conflicts[0].reason).toBe('conflict');

    // no double-assignment: assigned exactly once, single run, single lease holder
    const stored = await repo.getJob(job.id, PID);
    expect(stored?.stage).toBe('assigned');
    expect(stored?.attempts).toBe(1);
    expect(await repo.listRunsByJob(job.id)).toHaveLength(1);
    const lease = await repo.getLease(job.id);
    expect(lease?.leaseEpoch).toBe(1);
  });

  it('atomic claim: N concurrent contenders (stress) — exactly one ok, N-1 conflicts, no double-assignment', async () => {
    const N = 15;
    const { job } = await coord.submitJob(PID, input());
    const results = await Promise.all(
      Array.from({ length: N }, (_, i) =>
        coord.tryClaimJob(job, factory({ factoryId: `fac_${i}` }))
      )
    );
    expect(results.filter(r => r.ok)).toHaveLength(1);
    expect(results.filter(r => !r.ok && r.reason === 'conflict')).toHaveLength(N - 1);
    // exactly one run + one assigned job + leaseEpoch 1
    expect(await repo.listRunsByJob(job.id)).toHaveLength(1);
    const stored = await repo.getJob(job.id, PID);
    expect(stored?.stage).toBe('assigned');
    expect(stored?.attempts).toBe(1);
    expect((await repo.getLease(job.id))?.leaseEpoch).toBe(1);
  });

  it('claimNextJob: N concurrent claimers for a single queued job — exactly one succeeds', async () => {
    await coord.submitJob(PID, input());
    const N = 12;
    const results = await Promise.all(
      Array.from({ length: N }, (_, i) => coord.claimNextJob(factory({ factoryId: `f_${i}` })))
    );
    expect(results.filter(r => r !== null)).toHaveLength(1);
  });

  it('lease renew under contention — exactly one concurrent renewal wins', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    const epoch = claim!.job.leaseEpoch;
    const N = 10;
    const results = await Promise.all(
      Array.from({ length: N }, () => coord.renewLease(job.id, PID, epoch, 600))
    );
    expect(results.filter(r => r.ok)).toHaveLength(1);
    expect(results.filter(r => !r.ok && r.reason === 'conflict')).toHaveLength(N - 1);
  });

  // ── PRIORITY + AGE SELECTION ──
  it('claimNextJob returns highest-priority then oldest', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'low-old', priority: 'low' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'crit-new', priority: 'critical' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'med', priority: 'medium' }));

    const claim = await coord.claimNextJob(factory());
    expect(claim?.job.idempotencyKey).toBe('crit-new');
  });

  it('claimNextJob respects capability subset', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'needs-mac', capabilities: ['os:mac'] }));
    const noCaps = await coord.claimNextJob(factory({ capabilities: [] }));
    expect(noCaps).toBeNull();
    const withCaps = await coord.claimNextJob(factory({ capabilities: ['os:mac', 'has:git'] }));
    expect(withCaps?.job.idempotencyKey).toBe('needs-mac');
  });

  // ── DEPS GATING ──
  it('deps: a job with unmet deps is blocked and not claimable until the dep ships', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'A' }));
    const { job: b } = await coord.submitJob(PID, input({ idempotencyKey: 'B', deps: ['A'] }));
    expect(b.stage).toBe('blocked');

    // first claim returns A (B is blocked)
    const first = await coord.claimNextJob(factory());
    expect(first?.job.idempotencyKey).toBe('A');
    // B still not claimable
    expect(await coord.claimNextJob(factory())).toBeNull();

    // ship A → B becomes claimable
    const a = await repo.getJob(first!.job.id, PID);
    await repo.updateJob(a!.id, PID, { stage: 'shipped' });
    const second = await coord.claimNextJob(factory());
    expect(second?.job.idempotencyKey).toBe('B');
  });

  it('deps-mode soft: dep satisfied when dependency is in testing', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'A' }));
    const { job: c } = await coord.submitJob(
      PID,
      input({ idempotencyKey: 'C', deps: ['A'], depsMode: 'soft' })
    );
    expect(c.stage).toBe('blocked');
    const a = (await repo.findJobsByIdempotencyKey(PID, 'A'))[0];
    await repo.updateJob(a.id, PID, { stage: 'testing' });
    const unmet = await coord.unmetDeps((await repo.getJob(c.id, PID))!);
    expect(unmet).toEqual([]);
  });

  it('cycle detection: a cyclic submit is rejected', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'B', deps: ['A'] }));
    await expect(
      coord.submitJob(PID, input({ idempotencyKey: 'A', deps: ['B'] }))
    ).rejects.toBeInstanceOf(BadRequestError);
  });

  // ── FENCING ──
  it('fencing: a stale leaseEpoch is rejected; the current epoch succeeds', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
    const epoch = claim!.job.leaseEpoch; // 1

    const stale = await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: epoch - 1,
      stage: 'building',
    });
    expect(stale.ok).toBe(false);
    if (!stale.ok) expect(stale.reason).toBe('fenced');

    const current = await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: epoch,
      stage: 'building',
    });
    expect(current.ok).toBe(true);
    if (current.ok) expect(current.doc.stage).toBe('building');
  });

  // ── REAPER ──
  it('reaper: expired lease returns the job to queued, bumps epoch, preserves checkpoint; idempotent', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    const epoch0 = claim!.job.leaseEpoch;

    // worker checkpoints WIP, then dies; force the lease to be expired
    await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: epoch0,
      stage: 'building',
      checkpoint: { wipBranch: 'aq/wip/x', wipBase: 'base1', wipCommit: 'c1' },
    });
    const lease = await repo.getLease(job.id);
    await repo.revUpdateLease(job.id, lease!.rev, { expiresAt: '2000-01-01T00:00:00.000Z' });

    const res = await coord.reapExpiredLeases(new Date().toISOString());
    expect(res.reaped).toBe(1);

    const reclaimed = await repo.getJob(job.id, PID);
    expect(reclaimed?.stage).toBe('queued');
    expect(reclaimed?.leaseEpoch).toBe(epoch0 + 1); // fenced
    expect(reclaimed?.checkpoint?.wipBranch).toBe('aq/wip/x'); // preserved
    expect((await repo.getLease(job.id))?.status).toBe('expired');

    // idempotent — running again reaps nothing
    const again = await coord.reapExpiredLeases(new Date().toISOString());
    expect(again.reaped).toBe(0);
  });

  it('reaper: the zombie (old epoch) is fenced out after reclaim', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    const zombieEpoch = claim!.job.leaseEpoch;
    await coord.patchJobFenced(job.id, PID, { leaseEpoch: zombieEpoch, stage: 'building' });
    const lease = await repo.getLease(job.id);
    await repo.revUpdateLease(job.id, lease!.rev, { expiresAt: '2000-01-01T00:00:00.000Z' });
    await coord.reapExpiredLeases(new Date().toISOString());

    // zombie tries to write with its now-stale epoch -> fenced
    const zombie = await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: zombieEpoch,
      stage: 'shipped',
    });
    expect(zombie.ok).toBe(false);
    if (!zombie.ok) expect(zombie.reason).toBe('fenced');
  });

  // ── HEARTBEAT ──
  it('heartbeat updates lastHeartbeatAt/health; staleness is detectable', async () => {
    await coord.heartbeat({
      productId: PID,
      factoryId: 'fac_1',
      capabilities: ['os:mac'],
      health: 'ok',
      load: 1,
    });
    const fac = await repo.getFactory('fac_1', PID);
    expect(fac?.health).toBe('ok');
    expect(fac?.capabilities).toEqual(['os:mac']);
    expect(coord.isFactoryStale(fac!, Date.now(), 60_000)).toBe(false);
    const oldNow = new Date(fac!.lastHeartbeatAt).getTime() + 120_000;
    expect(coord.isFactoryStale(fac!, oldNow, 60_000)).toBe(true);
  });

  // ── IDEMPOTENCY ──
  it('idempotent submit: same key+content => 1 job', async () => {
    const a = await coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'same' }));
    const b = await coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'same' }));
    expect(b.outcome).toBe('deduplicated');
    expect(b.job.id).toBe(a.job.id);
    expect(await repo.findJobsByIdempotencyKey(PID, 'k')).toHaveLength(1);
  });

  it('idempotent submit: same key+changed content while queued => superseded', async () => {
    const a = await coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'v1' }));
    const b = await coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'v2' }));
    expect(b.outcome).toBe('superseded');
    expect(b.job.id).toBe(a.job.id);
    expect(b.job.bodyMd).toBe('v2');
    expect(await repo.findJobsByIdempotencyKey(PID, 'k')).toHaveLength(1);
  });

  it('idempotent submit: same key+changed content past queued => 409', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'v1' }));
    await coord.claimNextJob(factory()); // job now assigned (past queued)
    await expect(
      coord.submitJob(PID, input({ idempotencyKey: 'k', bodyMd: 'v2' }))
    ).rejects.toBeInstanceOf(ConflictError);
  });

  // ── §7 SCORE-DRIVEN SELECTION (Phase 2 scheduler wired into claimNextJob) ──
  it('selection now follows the §7 score: within-budget cost-fit beats an older same-priority job', async () => {
    // jobB is submitted first (older) and is the same priority, but its budget blows
    // the factory cost ceiling. The old priority+age rule would have taken the older
    // jobB; the scorer prefers the within-budget jobA.
    await coord.submitJob(
      PID,
      input({ idempotencyKey: 'B-old-expensive', priority: 'medium', budget: { usd: 100 } })
    );
    await coord.submitJob(
      PID,
      input({ idempotencyKey: 'A-new-cheap', priority: 'medium', budget: { usd: 5 } })
    );
    const claim = await coord.claimNextJob(factory({ costCeilingUsd: 10 }));
    expect(claim?.job.idempotencyKey).toBe('A-new-cheap');
  });

  it('claimNextJob: a factory below the health floor (down) claims nothing; a healthy one does', async () => {
    await coord.submitJob(PID, input());
    expect(await coord.claimNextJob(factory({ health: 'down' }))).toBeNull();
    const ok = await coord.claimNextJob(factory({ health: 'ok' }));
    expect(ok).not.toBeNull();
  });

  it('claimNextJob drains in score order: highest priority first, then the next, then null', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'low', priority: 'low' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'crit', priority: 'critical' }));
    expect((await coord.claimNextJob(factory({ factoryId: 'f1' })))?.job.idempotencyKey).toBe(
      'crit'
    );
    expect((await coord.claimNextJob(factory({ factoryId: 'f2' })))?.job.idempotencyKey).toBe(
      'low'
    );
    expect(await coord.claimNextJob(factory({ factoryId: 'f3' }))).toBeNull();
  });

  // ── Phase 3: TUNABLE WEIGHTS via coordinator ──
  it('weight registry: per-product weights flow into claimNextJob selection', async () => {
    coord.setWeightRegistry({ [PID]: { starvation: 0 } });
    await coord.submitJob(PID, input({ idempotencyKey: 'a', priority: 'medium' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'b', priority: 'medium' }));
    // With starvation=0, all scores equal → tie-break by age → older wins
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
    expect(claim!.job.idempotencyKey).toBe('a'); // older
    coord.setWeightRegistry({}); // reset
  });

  // ── Phase 3: PREEMPTION (coordinator integration) ──
  it('preemption OFF (default): critical job not placed returns null, no eviction', async () => {
    delete process.env.FLEET_PREEMPTION;
    // Submit a medium job and claim it (factory at capacity)
    await coord.submitJob(PID, input({ idempotencyKey: 'med', priority: 'medium' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();

    // Now submit a critical job — factory is busy, no more queued jobs claimable
    await coord.submitJob(PID, input({ idempotencyKey: 'crit', priority: 'critical' }));
    // With a second factory, the critical job CAN be claimed normally
    const claim2 = await coord.claimNextJob(factory({ factoryId: 'fac_2' }));
    expect(claim2!.job.idempotencyKey).toBe('crit');
  });

  it('preemption ON: critical job evicts lower-priority running job, victim requeued with checkpoint + bumped epoch', async () => {
    process.env.FLEET_PREEMPTION = '1';
    try {
      // Submit and claim a LOW priority job on fac_1 (requires os:linux)
      await coord.submitJob(
        PID,
        input({ idempotencyKey: 'low-running', priority: 'low', capabilities: ['os:linux'] })
      );
      const lowClaim = await coord.claimNextJob(factory({ capabilities: ['os:linux', 'os:mac'] }));
      expect(lowClaim).not.toBeNull();
      const lowEpoch = lowClaim!.job.leaseEpoch;

      // Worker adds a checkpoint
      await coord.patchJobFenced(lowClaim!.job.id, PID, {
        leaseEpoch: lowEpoch,
        stage: 'building',
        checkpoint: { wipBranch: 'aq/wip/low', wipBase: 'main', wipCommit: 'abc123' },
      });

      // Submit a CRITICAL job requiring os:mac — but the ONLY queued job now needs
      // a capability the factory has (os:mac). For selectJob to return null, we need
      // the critical job to NOT be in queued/blocked stage from selectJob's view.
      // Actually, let's use a different approach: submit critical with capability
      // that the factory HAS, but also submit a non-critical blocker first:
      // The real scenario: fac_1 is at seatLimit=1, already holding low-running.
      // We enforce seatLimit in the preemption path.
      await coord.submitJob(
        PID,
        input({ idempotencyKey: 'crit-urgent', priority: 'critical', capabilities: ['os:mac'] })
      );

      // A DIFFERENT factory (fac_2) with only os:mac claims the critical normally
      // This proves selectJob finds it for capable factories
      const fac2Claim = await coord.claimNextJob(
        factory({ factoryId: 'fac_2', capabilities: ['os:mac'] })
      );
      expect(fac2Claim!.job.idempotencyKey).toBe('crit-urgent');

      // Now submit another critical job
      await coord.submitJob(
        PID,
        input({
          idempotencyKey: 'crit-urgent-2',
          priority: 'critical',
          capabilities: ['os:linux'],
        })
      );

      // fac_1 tries to claim but it already holds low-running at seatLimit=1
      // With preemption ON and seatLimit enforcement, it should preempt
      const preemptClaim = await coord.claimNextJob(
        factory({ capabilities: ['os:linux', 'os:mac'], seatLimit: 1 })
      );
      expect(preemptClaim).not.toBeNull();
      expect(preemptClaim!.job.idempotencyKey).toBe('crit-urgent-2');

      // The victim (low-running) should be requeued with bumped epoch + checkpoint preserved
      const victim = await repo.getJob(lowClaim!.job.id, PID);
      expect(victim?.stage).toBe('queued');
      expect(victim?.leaseEpoch).toBeGreaterThan(lowEpoch);
      expect(victim?.checkpoint?.wipBranch).toBe('aq/wip/low'); // preserved

      // The zombie's stale report is fenced
      const zombieAttempt = await coord.patchJobFenced(lowClaim!.job.id, PID, {
        leaseEpoch: lowEpoch,
        stage: 'shipped',
      });
      expect(zombieAttempt.ok).toBe(false);
      if (!zombieAttempt.ok) expect(zombieAttempt.reason).toBe('fenced');

      // A preempted event should exist
      const events = await repo.listEvents(lowClaim!.job.id);
      expect(events.some(e => e.type === 'preempted')).toBe(true);
    } finally {
      delete process.env.FLEET_PREEMPTION;
    }
  });

  it('preemption ON: never preempts equal or higher priority', async () => {
    process.env.FLEET_PREEMPTION = '1';
    try {
      // Claim a CRITICAL job on fac_1
      await coord.submitJob(PID, input({ idempotencyKey: 'crit-running', priority: 'critical' }));
      await coord.claimNextJob(factory());

      // Submit another critical job
      await coord.submitJob(PID, input({ idempotencyKey: 'crit2', priority: 'critical' }));

      // fac_1 has a critical job running — should NOT preempt it for another critical
      // A second factory can claim normally though
      const claim = await coord.claimNextJob(factory({ factoryId: 'fac_2' }));
      expect(claim!.job.idempotencyKey).toBe('crit2');
    } finally {
      delete process.env.FLEET_PREEMPTION;
    }
  });
});

describe('fleet coordinator — Phase 3 DAG decomposition', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('fan-out: submitting a parent with children atomically creates children + blocks parent', async () => {
    const { job: parent } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'parent-1',
        kind: 'composite',
        children: [
          {
            idempotencyKey: 'child-a',
            bodyMd: '# child A',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
          {
            idempotencyKey: 'child-b',
            bodyMd: '# child B',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      })
    );
    expect(parent.kind).toBe('composite');
    expect(parent.stage).toBe('blocked');
    expect(parent.deps).toContain('child-a');
    expect(parent.deps).toContain('child-b');

    // Children exist and are queued
    const children = await repo.listChildrenByParent(PID, parent.id);
    expect(children).toHaveLength(2);
    expect(children.every(c => c.stage === 'queued')).toBe(true);
    expect(children.every(c => c.parentId === parent.id)).toBe(true);
  });

  it('parent is not claimable while children are still running', async () => {
    const { job: parent } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'parent-2',
        kind: 'composite',
        children: [
          {
            idempotencyKey: 'child-x',
            bodyMd: '# child X',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      })
    );
    // Parent is blocked — attempting to claim should skip it
    const claim = await coord.claimNextJob(factory());
    // Should claim the child, not the parent
    expect(claim).not.toBeNull();
    expect(claim!.job.parentId).toBe(parent.id);
    expect(claim!.job.idempotencyKey).toBe('child-x');
  });

  it('last child completion unblocks parent', async () => {
    const { job: parent } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'parent-3',
        kind: 'composite',
        children: [
          {
            idempotencyKey: 'child-1',
            bodyMd: '# c1',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
          {
            idempotencyKey: 'child-2',
            bodyMd: '# c2',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      })
    );

    // Claim and ship child-1
    const c1Claim = await coord.claimNextJob(factory({ factoryId: 'f1' }));
    expect(c1Claim!.job.idempotencyKey).toBe('child-1');
    await coord.patchJobFenced(c1Claim!.job.id, PID, {
      leaseEpoch: c1Claim!.job.leaseEpoch,
      stage: 'shipped',
    });

    // Parent still blocked (child-2 not done)
    const parentMid = await repo.getJob(parent.id, PID);
    expect(parentMid?.stage).toBe('blocked');

    // Claim and ship child-2
    const c2Claim = await coord.claimNextJob(factory({ factoryId: 'f2' }));
    expect(c2Claim!.job.idempotencyKey).toBe('child-2');
    await coord.patchJobFenced(c2Claim!.job.id, PID, {
      leaseEpoch: c2Claim!.job.leaseEpoch,
      stage: 'shipped',
    });

    // Parent should now be unblocked (queued)
    const parentAfter = await repo.getJob(parent.id, PID);
    expect(parentAfter?.stage).toBe('queued');
  });

  it('cycle at submit: parent depending on itself is rejected', async () => {
    await expect(
      coord.submitJob(
        PID,
        input({
          idempotencyKey: 'self-cycle',
          deps: ['self-cycle'],
        })
      )
    ).rejects.toBeInstanceOf(BadRequestError);
  });

  it('submitChildren: add children to an existing job', async () => {
    const { job: parent } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'parent-add',
        kind: 'leaf',
      })
    );
    const result = await coord.submitChildren(parent.id, PID, [
      { idempotencyKey: 'added-child', bodyMd: '# added', capabilities: [] },
    ]);
    expect(result.childJobs).toHaveLength(1);
    expect(result.parent.kind).toBe('composite');
    expect(result.parent.stage).toBe('blocked');
    expect(result.parent.deps).toContain('added-child');
  });

  it('getDagSubtree returns the correct tree structure', async () => {
    const { job: parent } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'dag-root',
        kind: 'composite',
        children: [
          {
            idempotencyKey: 'dag-c1',
            bodyMd: '# c1',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
          {
            idempotencyKey: 'dag-c2',
            bodyMd: '# c2',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      })
    );
    const dag = await coord.getDagSubtree(parent.id, PID);
    expect(dag).not.toBeNull();
    expect(dag!.id).toBe(parent.id);
    expect(dag!.kind).toBe('composite');
    expect(dag!.children).toHaveLength(2);
    expect(dag!.children[0].parentId).toBe(parent.id);
    expect(dag!.children[1].parentId).toBe(parent.id);
  });

  it('capability + priority still respected per DAG node', async () => {
    await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'dag-parent',
        kind: 'composite',
        children: [
          {
            idempotencyKey: 'dag-mac',
            bodyMd: '# mac',
            capabilities: ['os:mac'],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
          {
            idempotencyKey: 'dag-linux',
            bodyMd: '# linux',
            capabilities: ['os:linux'],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      })
    );
    // Factory with only os:linux can only claim the linux child
    const claim = await coord.claimNextJob(factory({ capabilities: ['os:linux'] }));
    expect(claim!.job.idempotencyKey).toBe('dag-linux');
  });

  it('all prior fleet tests remain green (non-DAG flows unaffected)', async () => {
    // Simple non-DAG submit + claim + ship still works
    const { job } = await coord.submitJob(PID, input({ idempotencyKey: 'simple' }));
    expect(job.stage).toBe('queued');
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
    expect(claim!.job.id).toBe(job.id);
  });
});

describe('fleet coordinator — Phase 3 per-product budgets', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    delete process.env.FLEET_BUDGETS;
  });

  it('under ceiling: claims proceed normally', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.submitJob(PID, input({ idempotencyKey: 'budget-ok' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
    expect(claim!.job.idempotencyKey).toBe('budget-ok');
  });

  it('crossing ceiling: auto-pauses and blocks claims', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 10, 'monthly');
    // Accrue spend up to ceiling
    await coord.accrueSpend(PID, 10);
    const budget = await coord.getBudget(PID);
    expect(budget?.status).toBe('paused');
    expect(budget?.spentUsd).toBe(10);

    // Claims should now be blocked
    await coord.submitJob(PID, input({ idempotencyKey: 'blocked-by-budget' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).toBeNull();
  });

  it('manual pause blocks claims', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.pauseBudget(PID);
    await coord.submitJob(PID, input({ idempotencyKey: 'paused-claim' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).toBeNull();
  });

  it('resume restores claims', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.pauseBudget(PID);
    await coord.resumeBudget(PID);
    await coord.submitJob(PID, input({ idempotencyKey: 'resumed-claim' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
  });

  it('flag OFF: no enforcement even with exceeded budget', async () => {
    delete process.env.FLEET_BUDGETS;
    await coord.upsertBudget(PID, 10, 'monthly');
    await coord.accrueSpend(PID, 20); // exceeded
    await coord.submitJob(PID, input({ idempotencyKey: 'flag-off-ok' }));
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
  });

  it('spend accounting is monotonic', async () => {
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.accrueSpend(PID, 5);
    await coord.accrueSpend(PID, 3);
    const budget = await coord.getBudget(PID);
    expect(budget?.spentUsd).toBe(8);
  });

  it('other products unaffected by one paused budget', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 10, 'monthly');
    await coord.pauseBudget(PID);
    // A different product can still claim
    const OTHER = 'chronomind';
    await coord.submitJob(OTHER, input({ idempotencyKey: 'other-ok' }));
    const claim = await coord.claimNextJob(factory({ productId: OTHER }));
    expect(claim).not.toBeNull();
  });

  // ── Phase 3: OPERATOR ACTIONS (requeue / reject / cancel) ──
  it('operator requeue: building job returns to queued, bumps epoch, fences the worker, preserves checkpoint', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    const epoch0 = claim!.job.leaseEpoch;
    await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: epoch0,
      stage: 'building',
      checkpoint: { wipBranch: 'aq/wip/op', wipBase: 'b1', wipCommit: 'c1' },
    });

    const res = await coord.operatorAction(job.id, PID, 'requeue');
    expect(res.ok).toBe(true);

    const after = await repo.getJob(job.id, PID);
    expect(after?.stage).toBe('queued');
    expect(after?.leaseEpoch).toBe(epoch0 + 1); // fenced
    expect(after?.checkpoint?.wipBranch).toBe('aq/wip/op'); // preserved
    expect((await repo.getLease(job.id))?.status).toBe('released');

    // the displaced worker (old epoch) can no longer write
    const zombie = await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: epoch0,
      stage: 'shipped',
    });
    expect(zombie.ok).toBe(false);
    if (!zombie.ok) expect(zombie.reason).toBe('fenced');
  });

  it('operator requeue: a job with unmet deps returns to blocked, not queued', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'dep' }));
    const { job } = await coord.submitJob(PID, input({ idempotencyKey: 'child', deps: ['dep'] }));
    expect(job.stage).toBe('blocked'); // dep is still queued, not done

    const res = await coord.operatorAction(job.id, PID, 'requeue');
    expect(res.ok).toBe(true);
    const after = await repo.getJob(job.id, PID);
    expect(after?.stage).toBe('blocked');
    expect(after?.blockedReason).toContain('dep');
  });

  it('operator reject: job moves to dead_letter', async () => {
    const { job } = await coord.submitJob(PID, input());
    await coord.claimNextJob(factory());
    const res = await coord.operatorAction(job.id, PID, 'reject');
    expect(res.ok).toBe(true);
    expect((await repo.getJob(job.id, PID))?.stage).toBe('dead_letter');
  });

  it('operator cancel: job moves to failed and emits an operator_action event', async () => {
    const { job } = await coord.submitJob(PID, input());
    await coord.claimNextJob(factory());
    const res = await coord.operatorAction(job.id, PID, 'cancel');
    expect(res.ok).toBe(true);
    expect((await repo.getJob(job.id, PID))?.stage).toBe('failed');
    const events = await repo.listEvents(job.id);
    const op = events.find(e => e.type === 'operator_action');
    expect(op?.data.action).toBe('cancel');
    expect(op?.actor).toBe('operator');
  });

  it('operator action: shipped job is terminal (invalid_state); unknown job is not_found', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: claim!.job.leaseEpoch,
      stage: 'shipped',
    });
    const shipped = await coord.operatorAction(job.id, PID, 'requeue');
    expect(shipped.ok).toBe(false);
    if (!shipped.ok) expect(shipped.reason).toBe('invalid_state');

    const missing = await coord.operatorAction('nope', PID, 'requeue');
    expect(missing.ok).toBe(false);
    if (!missing.ok) expect(missing.reason).toBe('not_found');
  });

  it('operator action: ship transitions a testing job to shipped (idempotent)', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    // simulate the review gate having routed the job to `testing` (no lease holder)
    await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: claim!.job.leaseEpoch,
      stage: 'testing',
    });

    const shipped = await coord.operatorAction(job.id, PID, 'ship');
    expect(shipped.ok).toBe(true);
    if (shipped.ok) expect(shipped.doc.stage).toBe('shipped');

    // re-shipping is a no-op (idempotent), not invalid_state
    const again = await coord.operatorAction(job.id, PID, 'ship');
    expect(again.ok).toBe(true);
    if (again.ok) expect(again.doc.stage).toBe('shipped');
  });

  it('releaseLease records run insights + result on the current run', async () => {
    const { job } = await coord.submitJob(PID, input());
    const claim = await coord.claimNextJob(factory());
    const epoch = claim!.job.leaseEpoch;

    const insights = { model: 'claude-sonnet-4', tokensIn: 1200, tokensOut: 340, costUsd: 0.0123 };
    const res = await coord.releaseLease(job.id, PID, epoch, 'shipped', {
      insights,
      result: 'shipped',
    });
    expect(res.ok).toBe(true);

    const runs = await repo.listRunsByJob(job.id);
    expect(runs.length).toBe(1);
    expect(runs[0]!.insights).toMatchObject(insights);
    expect(runs[0]!.result).toBe('shipped');
    expect(runs[0]!.endedAt).toBeTruthy();
  });

  it('operator reject/cancel are idempotent on already-terminal jobs (no epoch bump)', async () => {
    const { job } = await coord.submitJob(PID, input());
    await coord.claimNextJob(factory());
    await coord.operatorAction(job.id, PID, 'reject');
    const afterFirst = await repo.getJob(job.id, PID);
    const epochAfterReject = afterFirst!.leaseEpoch;

    // re-reject is a no-op: stage stays dead_letter, epoch unchanged, no new event
    const again = await coord.operatorAction(job.id, PID, 'reject');
    expect(again.ok).toBe(true);
    const afterSecond = await repo.getJob(job.id, PID);
    expect(afterSecond?.stage).toBe('dead_letter');
    expect(afterSecond?.leaseEpoch).toBe(epochAfterReject);
    const rejectEvents = (await repo.listEvents(job.id)).filter(e => e.type === 'operator_action');
    expect(rejectEvents).toHaveLength(1);
  });

  // ── Phase 3: SCORING EXPLAINABILITY ──
  it('explainJob: returns per-factory score breakdowns, eligibility, and the best factory', async () => {
    const { job } = await coord.submitJob(PID, input({ capabilities: ['os:mac'] }));
    // a capable factory and an incapable one both heartbeat in
    await coord.heartbeat({
      productId: PID,
      factoryId: 'cap',
      capabilities: ['os:mac', 'has:git'],
      health: 'ok',
      load: 0,
    });
    await coord.heartbeat({
      productId: PID,
      factoryId: 'nocap',
      capabilities: ['os:linux'],
      health: 'ok',
      load: 5,
    });

    const explain = await coord.explainJob(job.id, PID);
    expect(explain).not.toBeNull();
    expect(explain!.jobId).toBe(job.id);
    expect(explain!.depsSatisfied).toBe(true);
    expect(explain!.factories).toHaveLength(2);
    // each breakdown's six weighted terms sum to the reported score
    for (const f of explain!.factories) {
      const sum =
        f.breakdown.capabilityFit +
        f.breakdown.affinity +
        f.breakdown.load +
        f.breakdown.costFit +
        f.breakdown.health +
        f.breakdown.starvation;
      expect(f.score).toBeCloseTo(sum, 9);
    }
    const cap = explain!.factories.find(f => f.factoryId === 'cap');
    const nocap = explain!.factories.find(f => f.factoryId === 'nocap');
    expect(cap?.eligible).toBe(true);
    expect(nocap?.eligible).toBe(false);
    expect(nocap?.ineligibleReasons).toContain('missing required capabilities');
    expect(explain!.bestFactoryId).toBe('cap');
  });

  it('explainJob: reports unmet deps and no eligible factory; unknown job is null', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'dep' }));
    const { job } = await coord.submitJob(PID, input({ idempotencyKey: 'child', deps: ['dep'] }));
    await coord.heartbeat({
      productId: PID,
      factoryId: 'f1',
      capabilities: [],
      health: 'ok',
      load: 0,
    });

    const explain = await coord.explainJob(job.id, PID);
    expect(explain!.depsSatisfied).toBe(false);
    expect(explain!.unmetDeps).toContain('dep');
    expect(explain!.bestFactoryId).toBeNull(); // deps unmet ⇒ nothing eligible
    expect(explain!.factories[0].ineligibleReasons.some(r => r.includes('unmet deps'))).toBe(true);

    expect(await coord.explainJob('missing', PID)).toBeNull();
  });

  // ── Phase 3: COST BURNDOWN ──
  it('costBurndown: aggregates run cost by UTC day with a gap-free cumulative series', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 50, 'monthly');
    const { job } = await coord.submitJob(PID, input());
    const today = new Date().toISOString();
    const yesterday = new Date(Date.now() - 86_400_000).toISOString();

    await repo.createRun({
      id: `${job.id}:run:1`,
      productId: PID,
      jobId: job.id,
      attempt: 1,
      engine: 'codex',
      startedAt: yesterday,
      endedAt: yesterday,
      insights: { costUsd: 4 },
    });
    await repo.createRun({
      id: `${job.id}:run:2`,
      productId: PID,
      jobId: job.id,
      attempt: 2,
      engine: 'codex',
      startedAt: today,
      endedAt: today,
      insights: { costUsd: 6 },
    });

    const burndown = await coord.costBurndown(PID, 7);
    expect(burndown.ceilingUsd).toBe(50);
    expect(burndown.days).toHaveLength(7); // gap-free window
    expect(burndown.totalUsd).toBe(10);
    // cumulative is monotonic non-decreasing and ends at the total
    const last = burndown.days[burndown.days.length - 1];
    expect(last.cumulativeUsd).toBe(10);
    for (let i = 1; i < burndown.days.length; i++) {
      expect(burndown.days[i].cumulativeUsd).toBeGreaterThanOrEqual(
        burndown.days[i - 1].cumulativeUsd
      );
    }
  });

  it('costBurndown: no budget + no runs yields a zeroed series and null ceiling', async () => {
    const burndown = await coord.costBurndown('emptyproduct', 5);
    expect(burndown.ceilingUsd).toBeNull();
    expect(burndown.days).toHaveLength(5);
    expect(burndown.totalUsd).toBe(0);
    expect(burndown.days.every(d => d.costUsd === 0)).toBe(true);
  });

  // ── FLEET METRICS + ALERTING (§17) ──
  it('fleetMetrics: empty fleet reports zeros and no alerts', async () => {
    const m = await coord.fleetMetrics('emptyproduct');
    expect(m.jobs.total).toBe(0);
    expect(m.jobs.queueDepth).toBe(0);
    expect(m.jobs.oldestQueuedAgeMs).toBeNull();
    expect(m.factories.total).toBe(0);
    expect(m.factories.utilizationPct).toBe(0);
    expect(m.alerts).toEqual([]);
  });

  it('fleetMetrics: counts stages, seats, and utilization', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'q1' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'q2' }));
    await coord.heartbeat({
      productId: PID,
      factoryId: 'fac_1',
      health: 'ok',
      load: 1,
      seatLimit: 2,
    });
    await coord.heartbeat({
      productId: PID,
      factoryId: 'fac_2',
      health: 'degraded',
      load: 2,
      seatLimit: 2,
    });

    const m = await coord.fleetMetrics(PID);
    expect(m.jobs.total).toBe(2);
    expect(m.jobs.queueDepth).toBe(2);
    expect(m.jobs.byStage.queued).toBe(2);
    expect(m.factories.total).toBe(2);
    expect(m.factories.byHealth).toEqual({ ok: 1, degraded: 1, down: 0 });
    expect(m.factories.seatsUsed).toBe(3);
    expect(m.factories.seatsTotal).toBe(4);
    expect(m.factories.utilizationPct).toBe(75);
  });

  it('fleetMetrics: raises no_live_capacity when jobs are queued with no healthy factory', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'q1' }));
    const m = await coord.fleetMetrics(PID);
    expect(m.alerts.some(a => a.code === 'no_live_capacity' && a.level === 'critical')).toBe(true);
  });

  it('fleetMetrics: raises all_factories_down and stale_factories', async () => {
    await coord.heartbeat({
      productId: PID,
      factoryId: 'fac_1',
      health: 'down',
      load: 0,
      seatLimit: 1,
    });
    // Force staleness by evaluating far in the future.
    const future = Date.now() + 10 * 60_000;
    const m = await coord.fleetMetrics(PID, { nowMs: future, staleMaxAgeMs: 60_000 });
    expect(m.factories.stale).toBe(1);
    expect(m.factories.live).toBe(0);
    expect(m.alerts.some(a => a.code === 'all_factories_down')).toBe(true);
    expect(m.alerts.some(a => a.code === 'stale_factories')).toBe(true);
  });

  it('fleetMetrics: raises queue_starvation and saturated under load', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'q1' }));
    await coord.heartbeat({
      productId: PID,
      factoryId: 'fac_1',
      health: 'ok',
      load: 1,
      seatLimit: 1,
    });
    // Evaluate 20 minutes later so the queued job exceeds the 1s starvation bound.
    const later = Date.now() + 20 * 60_000;
    const m = await coord.fleetMetrics(PID, {
      nowMs: later,
      starvationMs: 1000,
      staleMaxAgeMs: 60 * 60_000,
    });
    expect(m.alerts.some(a => a.code === 'queue_starvation')).toBe(true);
    expect(m.alerts.some(a => a.code === 'saturated')).toBe(true);
  });

  it('fleetMetrics: no budget configured ⇒ budget summary is null', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'nb' }));
    const m = await coord.fleetMetrics(PID);
    expect(m.budget).toBeNull();
    expect(m.alerts.some(a => a.code.startsWith('budget'))).toBe(false);
  });

  it('fleetMetrics: surfaces a budget summary and projected-overspend warning', async () => {
    const start = Date.parse('2026-06-01T00:00:00.000Z');
    await coord.upsertBudget(PID, 100, 'monthly');
    await repo.updateBudget(PID, { windowStart: new Date(start).toISOString() });
    await coord.accrueSpend(PID, 30, 'r1'); // 30 spent
    // 3 days into a 30-day window (10% elapsed) ⇒ project 30 / 0.1 = 300 > 100.
    const now = start + 3 * 86_400_000;
    const m = await coord.fleetMetrics(PID, { nowMs: now });
    expect(m.budget).toMatchObject({ ceilingUsd: 100, spentUsd: 30, status: 'active' });
    expect(m.budget?.projectedUsd).toBeGreaterThan(100);
    expect(
      m.alerts.some(a => a.code === 'budget_overspend_projected' && a.level === 'warning')
    ).toBe(true);
  });

  it('fleetMetrics: an on-track budget raises no projected-overspend alert', async () => {
    const start = Date.parse('2026-06-01T00:00:00.000Z');
    await coord.upsertBudget(PID, 100, 'monthly');
    await repo.updateBudget(PID, { windowStart: new Date(start).toISOString() });
    await coord.accrueSpend(PID, 5, 'r1'); // 5 spent, 50% elapsed ⇒ project 10 < 100
    const now = start + 15 * 86_400_000;
    const m = await coord.fleetMetrics(PID, { nowMs: now });
    expect(m.alerts.some(a => a.code === 'budget_overspend_projected')).toBe(false);
  });

  it('fleetMetrics: an exhausted budget raises a critical alert', async () => {
    await coord.upsertBudget(PID, 10, 'monthly');
    await coord.accrueSpend(PID, 10, 'r1'); // hits ceiling
    const m = await coord.fleetMetrics(PID);
    expect(m.alerts.some(a => a.code === 'budget_exhausted' && a.level === 'critical')).toBe(true);
  });

  it('fleetMetrics: a per-engine sub-ceiling reached raises engine_budget_exhausted', async () => {
    await coord.upsertBudget(PID, 1000, 'monthly', { codex: 5 });
    await coord.accrueSpend(PID, 5, 'r1', 'codex');
    const m = await coord.fleetMetrics(PID);
    expect(m.budget?.engines).toContainEqual(
      expect.objectContaining({ engine: 'codex', exhausted: true })
    );
    expect(m.alerts.some(a => a.code === 'engine_budget_exhausted')).toBe(true);
  });

  // ── MULTI-REVIEWER HUMAN GATE (§14 Phase 3) ──
  async function toBuilding(idempotencyKey = 'rev-1') {
    const { job } = await coord.submitJob(PID, input({ idempotencyKey }));
    const claim = await coord.claimNextJob(factory());
    await coord.patchJobFenced(job.id, PID, {
      leaseEpoch: claim!.job.leaseEpoch,
      stage: 'building',
    });
    return job;
  }

  it('requestReview: only a building job can enter review; fences the worker', async () => {
    const job = await toBuilding();
    const before = await repo.getJob(job.id, PID);
    const res = await coord.requestReview(job.id, PID, { requiredApprovals: 1 });
    expect(res.ok).toBe(true);
    const after = await repo.getJob(job.id, PID);
    expect(after?.stage).toBe('review');
    expect(after?.leaseEpoch).toBe(before!.leaseEpoch + 1);
    expect((await repo.getLease(job.id))?.status).toBe('released');
  });

  it('requestReview: a queued job cannot be routed to review', async () => {
    const { job } = await coord.submitJob(PID, input());
    const res = await coord.requestReview(job.id, PID);
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('invalid_state');
  });

  it('requestReview: rejects an unsatisfiable policy (quorum > allowlist size)', async () => {
    const job = await toBuilding();
    const res = await coord.requestReview(job.id, PID, {
      requiredApprovals: 2,
      reviewers: ['alice@x.com'],
    });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('invalid_policy');
  });

  it('submitReview: single approval advances the job to testing', async () => {
    const job = await toBuilding();
    await coord.requestReview(job.id, PID, { requiredApprovals: 1 });
    const res = await coord.submitReview(job.id, PID, {
      reviewer: 'Alice@X.com',
      decision: 'approve',
    });
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.gate).toBe('approved');
    const after = await repo.getJob(job.id, PID);
    expect(after?.stage).toBe('testing');
    expect(after?.reviewDecisions?.[0].reviewer).toBe('alice@x.com'); // normalized
  });

  it('submitReview: quorum of 2 needs two distinct approvers', async () => {
    const job = await toBuilding();
    await coord.requestReview(job.id, PID, {
      requiredApprovals: 2,
      reviewers: ['alice@x.com', 'bob@x.com'],
    });
    const r1 = await coord.submitReview(job.id, PID, {
      reviewer: 'alice@x.com',
      decision: 'approve',
    });
    expect(r1.ok && r1.gate).toBe('pending');
    expect((await repo.getJob(job.id, PID))?.stage).toBe('review');
    // Same reviewer approving again does not satisfy the quorum.
    const dup = await coord.submitReview(job.id, PID, {
      reviewer: 'alice@x.com',
      decision: 'approve',
    });
    expect(dup.ok && dup.gate).toBe('pending');
    expect((await repo.getJob(job.id, PID))?.stage).toBe('review');
    const r2 = await coord.submitReview(job.id, PID, {
      reviewer: 'bob@x.com',
      decision: 'approve',
    });
    expect(r2.ok && r2.gate).toBe('approved');
    expect((await repo.getJob(job.id, PID))?.stage).toBe('testing');
  });

  it('submitReview: any reject vetoes and returns the job to queued for rework', async () => {
    const job = await toBuilding();
    await coord.requestReview(job.id, PID, { requiredApprovals: 2 });
    const res = await coord.submitReview(job.id, PID, {
      reviewer: 'carol@x.com',
      decision: 'reject',
      note: 'needs tests',
    });
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.gate).toBe('rejected');
    const after = await repo.getJob(job.id, PID);
    expect(after?.stage).toBe('queued');
    expect(after?.blockedReason).toContain('needs tests');
  });

  it('submitReview: a non-allowlisted reviewer is rejected', async () => {
    const job = await toBuilding();
    await coord.requestReview(job.id, PID, { requiredApprovals: 1, reviewers: ['alice@x.com'] });
    const res = await coord.submitReview(job.id, PID, {
      reviewer: 'mallory@x.com',
      decision: 'approve',
    });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('not_authorized');
  });

  it('submitReview: a job not in review cannot be reviewed', async () => {
    const { job } = await coord.submitJob(PID, input());
    const res = await coord.submitReview(job.id, PID, {
      reviewer: 'alice@x.com',
      decision: 'approve',
    });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('invalid_state');
  });
});

// ── Phase 3 fix-ups: idempotent accrual + ship-wiring + batch-aware cycle detection ──
describe('fleet coordinator — budget accrual idempotency + ship wiring', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    delete process.env.FLEET_BUDGETS;
  });

  it('accrueSpend is idempotent per runId (same run never double-counts)', async () => {
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.accrueSpend(PID, 5, 'run-x');
    await coord.accrueSpend(PID, 5, 'run-x'); // duplicate run — no-op
    expect((await coord.getBudget(PID))?.spentUsd).toBe(5);
    await coord.accrueSpend(PID, 5, 'run-y'); // distinct run — accrues
    expect((await coord.getBudget(PID))?.spentUsd).toBe(10);
  });

  it("shipping a job accrues the run's insights.costUsd exactly once (FLEET_BUDGETS on)", async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.submitJob(PID, input({ idempotencyKey: 'ship-1' }));
    const claim = await coord.claimNextJob(factory());
    const claimed = claim!.job;
    // factory reports the run's actual cost (what costBurndown + accrual read)
    const runs = await repo.listRunsByJob(claimed.id);
    await repo.updateRun(runs[0]!.id, claimed.id, { insights: { costUsd: 7 } });
    // ship twice with the same lease epoch — must accrue only once
    await coord.patchJobFenced(claimed.id, PID, {
      leaseEpoch: claimed.leaseEpoch,
      stage: 'shipped',
    });
    await coord.patchJobFenced(claimed.id, PID, {
      leaseEpoch: claimed.leaseEpoch,
      stage: 'shipped',
    });
    expect((await coord.getBudget(PID))?.spentUsd).toBe(7);
  });

  it('shipping does NOT accrue when FLEET_BUDGETS is off', async () => {
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.submitJob(PID, input({ idempotencyKey: 'ship-off' }));
    const claim = await coord.claimNextJob(factory());
    const claimed = claim!.job;
    const runs = await repo.listRunsByJob(claimed.id);
    await repo.updateRun(runs[0]!.id, claimed.id, { insights: { costUsd: 7 } });
    await coord.patchJobFenced(claimed.id, PID, {
      leaseEpoch: claimed.leaseEpoch,
      stage: 'shipped',
    });
    expect((await coord.getBudget(PID))?.spentUsd).toBe(0);
  });
});

describe('fleet coordinator §3 — per-engine budget ceilings', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    delete process.env.FLEET_BUDGETS;
  });

  it('projectBudgetSpend: extrapolates burn rate, caps fraction, and guards early window', () => {
    const start = '2026-06-01T00:00:00.000Z';
    const t0 = Date.parse(start);
    const b = { spentUsd: 30, window: 'monthly' as const, windowStart: start };
    // 3 days into 30 (10% elapsed) ⇒ 30 / 0.1 = 300
    expect(coord.projectBudgetSpend(b, t0 + 3 * 86_400_000)?.projectedUsd).toBeCloseTo(300);
    // beyond the window the fraction caps at 1 ⇒ projection = actual spend
    expect(coord.projectBudgetSpend(b, t0 + 60 * 86_400_000)?.projectedUsd).toBeCloseTo(30);
    // too early (under minFraction) ⇒ no projection (avoid crying wolf)
    expect(coord.projectBudgetSpend(b, t0 + 3_600_000)).toBeNull();
    // no windowStart ⇒ cannot project
    expect(coord.projectBudgetSpend({ spentUsd: 5, window: 'daily' }, t0)).toBeNull();
  });

  it('upsert stores per-engine ceilings and preserves them on a re-upsert', async () => {
    await coord.upsertBudget(PID, 100, 'monthly', { codex: 20 });
    expect((await coord.getBudget(PID))?.engineCeilingsUsd).toEqual({ codex: 20 });
    // re-upsert WITHOUT engine ceilings preserves the prior ones (and spend)
    await coord.accrueSpend(PID, 3, 'r1', 'codex');
    await coord.upsertBudget(PID, 200, 'monthly');
    const b = await coord.getBudget(PID);
    expect(b?.ceilingUsd).toBe(200);
    expect(b?.engineCeilingsUsd).toEqual({ codex: 20 });
    expect(b?.spentByEngineUsd).toEqual({ codex: 3 });
  });

  it('accrueSpend tracks per-engine spend alongside the total (idempotent per run)', async () => {
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.accrueSpend(PID, 4, 'r1', 'codex');
    await coord.accrueSpend(PID, 4, 'r1', 'codex'); // duplicate run — no double count
    await coord.accrueSpend(PID, 6, 'r2', 'devin');
    const b = await coord.getBudget(PID);
    expect(b?.spentUsd).toBe(10);
    expect(b?.spentByEngineUsd).toEqual({ codex: 4, devin: 6 });
  });

  it('shipping accrues per-engine spend keyed by the run engine (FLEET_BUDGETS on)', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 100, 'monthly');
    await coord.submitJob(PID, input({ idempotencyKey: 'ship-eng', engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    const claimed = claim!.job;
    const runs = await repo.listRunsByJob(claimed.id);
    await repo.updateRun(runs[0]!.id, claimed.id, { insights: { engine: 'codex', costUsd: 9 } });
    await coord.patchJobFenced(claimed.id, PID, {
      leaseEpoch: claimed.leaseEpoch,
      stage: 'shipped',
    });
    expect((await coord.getBudget(PID))?.spentByEngineUsd).toEqual({ codex: 9 });
  });

  it('an engine at its sub-ceiling is routed around at claim time (FLEET_BUDGETS on)', async () => {
    process.env.FLEET_BUDGETS = '1';
    // product ceiling high (no pause); codex sub-ceiling reached.
    await coord.upsertBudget(PID, 1000, 'monthly', { codex: 5 });
    await coord.accrueSpend(PID, 5, 'seed', 'codex'); // codex spend hits its ceiling
    await coord.submitJob(PID, input({ idempotencyKey: 'c', engine: 'codex' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'd', engine: 'devin' }));
    const claim = await coord.claimNextJob(
      factory({ capabilities: ['engine:codex', 'engine:devin'] })
    );
    expect(claim?.job.engine).toBe('devin'); // codex blocked, devin uncapped
  });

  it('a sole engine at its sub-ceiling yields no claim (FLEET_BUDGETS on)', async () => {
    process.env.FLEET_BUDGETS = '1';
    await coord.upsertBudget(PID, 1000, 'monthly', { codex: 5 });
    await coord.accrueSpend(PID, 5, 'seed', 'codex');
    await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    expect(claim).toBeNull();
  });

  it('with FLEET_BUDGETS off, per-engine ceilings are ignored (default behavior)', async () => {
    delete process.env.FLEET_BUDGETS;
    await coord.upsertBudget(PID, 1000, 'monthly', { codex: 5 });
    await coord.accrueSpend(PID, 5, 'seed', 'codex');
    await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    expect(claim?.job.engine).toBe('codex');
  });
});

describe('fleet coordinator — DAG submitChildren cycle detection', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('rejects a child that depends on itself', async () => {
    const { job: parent } = await coord.submitJob(PID, input({ idempotencyKey: 'p-self' }));
    await expect(
      coord.submitChildren(parent.id, PID, [
        { idempotencyKey: 'c-self', bodyMd: '# c', deps: ['c-self'] },
      ])
    ).rejects.toThrow(BadRequestError);
  });

  it('rejects a child that depends on the parent (child-parent cycle)', async () => {
    const { job: parent } = await coord.submitJob(PID, input({ idempotencyKey: 'p-back' }));
    await expect(
      coord.submitChildren(parent.id, PID, [
        { idempotencyKey: 'c-back', bodyMd: '# c', deps: ['p-back'] },
      ])
    ).rejects.toThrow(BadRequestError);
  });

  it('rejects mutually-dependent siblings (child-sibling cycle)', async () => {
    const { job: parent } = await coord.submitJob(PID, input({ idempotencyKey: 'p-sib' }));
    await expect(
      coord.submitChildren(parent.id, PID, [
        { idempotencyKey: 'c-a', bodyMd: '# a', deps: ['c-b'] },
        { idempotencyKey: 'c-b', bodyMd: '# b', deps: ['c-a'] },
      ])
    ).rejects.toThrow(BadRequestError);
  });

  it('rejects duplicate child idempotency-keys in one batch', async () => {
    const { job: parent } = await coord.submitJob(PID, input({ idempotencyKey: 'p-dup' }));
    await expect(
      coord.submitChildren(parent.id, PID, [
        { idempotencyKey: 'dup', bodyMd: '# a' },
        { idempotencyKey: 'dup', bodyMd: '# b' },
      ])
    ).rejects.toThrow(BadRequestError);
  });

  it('accepts a valid acyclic fan-out (sibling dependency, no cycle)', async () => {
    const { job: parent } = await coord.submitJob(PID, input({ idempotencyKey: 'p-ok' }));
    const result = await coord.submitChildren(parent.id, PID, [
      { idempotencyKey: 'step-1', bodyMd: '# 1' },
      { idempotencyKey: 'step-2', bodyMd: '# 2', deps: ['step-1'] },
    ]);
    expect(result.childJobs).toHaveLength(2);
    expect(result.parent.stage).toBe('blocked');
  });
});

describe('fleet coordinator — PR target + deliverable (§PR mode)', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('submit records repo/baseBranch on the job', async () => {
    await coord.submitJob(
      PID,
      input({ idempotencyKey: 'pr-1', repo: 'acme/widgets', baseBranch: 'develop' })
    );
    const claim = await coord.claimNextJob(factory());
    expect(claim!.job.repo).toBe('acme/widgets');
    expect(claim!.job.baseBranch).toBe('develop');
  });

  it('release report records prUrl + branch on the run', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'pr-2', repo: 'acme/widgets' }));
    const claim = await coord.claimNextJob(factory());
    const c = claim!.job;
    const rel = await coord.releaseLease(c.id, PID, c.leaseEpoch, 'review', {
      result: 'shipped',
      prUrl: 'https://github.com/acme/widgets/pull/42',
      branch: 'aq/job/pr-2',
    });
    expect(rel.ok).toBe(true);
    const runs = await repo.listRunsByJob(c.id);
    expect(runs[0]!.prUrl).toBe('https://github.com/acme/widgets/pull/42');
    expect(runs[0]!.branch).toBe('aq/job/pr-2');
  });
});

describe('fleet coordinator — PR-state reconcile', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('mapGhPrState maps GitHub states (MERGED/OPEN/other)', () => {
    expect(coord.mapGhPrState('MERGED')).toBe('merged');
    expect(coord.mapGhPrState('merged')).toBe('merged');
    expect(coord.mapGhPrState('OPEN')).toBe('open');
    expect(coord.mapGhPrState('CLOSED')).toBeNull();
    expect(coord.mapGhPrState(undefined)).toBeNull();
  });

  async function setupOpenPrJob() {
    await coord.submitJob(PID, input({ idempotencyKey: 'pr-rec', repo: 'acme/widgets' }));
    const claim = await coord.claimNextJob(factory());
    const c = claim!.job;
    await coord.releaseLease(c.id, PID, c.leaseEpoch, 'review', {
      result: 'shipped',
      prUrl: 'https://github.com/acme/widgets/pull/3',
      prState: 'open',
    });
    return c.id;
  }

  it('flips prState to merged + appends a pr_merged event when GitHub reports MERGED', async () => {
    const jobId = await setupOpenPrJob();
    const res = await coord.reconcileJobPrState(jobId, PID, async () => 'merged');
    expect(res).toMatchObject({ ok: true, updated: true, prState: 'merged' });
    const runs = await repo.listRunsByJob(jobId);
    expect(runs.find(r => r.prUrl)!.prState).toBe('merged');
    const events = await repo.listEvents(jobId);
    expect(events.some(e => e.type === 'pr_merged' && e.data?.via === 'reconcile')).toBe(true);
  });

  it('is a no-op when GitHub still reports the PR open', async () => {
    const jobId = await setupOpenPrJob();
    const res = await coord.reconcileJobPrState(jobId, PID, async () => 'open');
    expect(res).toMatchObject({ ok: true, updated: false, reason: 'unchanged' });
    const runs = await repo.listRunsByJob(jobId);
    expect(runs.find(r => r.prUrl)!.prState).toBe('open');
  });

  it('returns no_pr when the job has no PR run, and not_found for an unknown job', async () => {
    await coord.submitJob(PID, input({ idempotencyKey: 'no-pr' }));
    const noPr = await coord.reconcileJobPrState(
      (await repo.findJobsByIdempotencyKey(PID, 'no-pr'))[0]!.id,
      PID,
      async () => 'merged'
    );
    expect(noPr).toMatchObject({ ok: true, updated: false, reason: 'no_pr' });
    const missing = await coord.reconcileJobPrState('fjob_missing', PID, async () => 'merged');
    expect(missing).toMatchObject({ ok: false, reason: 'not_found' });
  });
});

describe('fleet coordinator — draft lifecycle (save / edit / submit)', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => _resetDatastoreProvider());

  it('submit with draft:true creates a draft that is NOT claimable', async () => {
    const { job, outcome } = await coord.submitJob(PID, input({ draft: true }));
    expect(outcome).toBe('created');
    expect(job.stage).toBe('draft');
    // a factory must not be able to claim a draft
    const claim = await coord.claimNextJob(factory());
    expect(claim).toBeNull();
  });

  it('updateDraft edits the prompt + config while draft (content hash changes)', async () => {
    const { job } = await coord.submitJob(PID, input({ draft: true }));
    const before = (await repo.getJob(job.id, PID))!.contentHash;
    const verBefore = await repo.getQueueVersion(PID);
    const res = await coord.updateDraft(job.id, PID, {
      bodyMd: '# edited prompt',
      repo: 'learning_ai_notes',
      autoMerge: true,
      priority: 'high',
    });
    expect(res.ok).toBe(true);
    if (res.ok) {
      expect(res.doc.bodyMd).toBe('# edited prompt');
      expect(res.doc.repo).toBe('learning_ai_notes');
      expect(res.doc.autoMerge).toBe(true);
      expect(res.doc.priority).toBe('high');
      expect(res.doc.contentHash).not.toBe(before);
      expect(res.doc.stage).toBe('draft'); // editing doesn't submit it
    }
    // the edit bumps the M0 queue gate so gated factories re-evaluate promptly
    expect(await repo.getQueueVersion(PID)).toBeGreaterThan(verBefore);
  });

  it('submitDraft promotes draft → queued, then it is claimable', async () => {
    const { job } = await coord.submitJob(PID, input({ draft: true }));
    const res = await coord.submitDraft(job.id, PID);
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.doc.stage).toBe('queued');
    const claim = await coord.claimNextJob(factory());
    expect(claim?.job.id).toBe(job.id);
  });

  it('submitDraft is idempotent on an already-submitted job', async () => {
    const { job } = await coord.submitJob(PID, input()); // queued, not a draft
    const res = await coord.submitDraft(job.id, PID);
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.doc.stage).toBe('queued');
  });

  it('updateDraft is rejected once the job is picked up (assigned+)', async () => {
    const { job } = await coord.submitJob(PID, input());
    await coord.claimNextJob(factory()); // job → assigned
    const res = await coord.updateDraft(job.id, PID, { bodyMd: '# too late' });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('conflict');
  });

  it('updateDraft on a missing job returns not_found', async () => {
    const res = await coord.updateDraft('nope', PID, { bodyMd: 'x' });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('not_found');
  });
});

// ── §2: per-engine circuit breaker + cost/latency-aware routing ───────────────

import { engineBreaker, getEngineBreakerSnapshot } from './engine-breaker.js';

const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

describe('fleet coordinator §2 — engine circuit breaker', () => {
  beforeEach(() => {
    setProvider(new MemoryDatastoreProvider());
    engineBreaker.clear();
  });
  afterEach(() => {
    _resetDatastoreProvider();
    engineBreaker.clear();
    delete process.env.FLEET_ENGINE_BREAKER;
  });

  it('records a failed attempt against the (factory, engine) pair on release', async () => {
    const { job } = await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    await coord.releaseLease(job.id, PID, claim!.job.leaseEpoch, 'failed', {
      result: 'failed',
      insights: { engine: 'codex' },
    });
    const snap = getEngineBreakerSnapshot();
    expect(snap).toContainEqual(
      expect.objectContaining({ factoryId: 'fac_1', engine: 'codex', failureCount: 1 })
    );
  });

  it('a shipped release records success and resets the pair', async () => {
    engineBreaker.recordFailure('fac_1', 'codex');
    const { job } = await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    await coord.releaseLease(job.id, PID, claim!.job.leaseEpoch, 'shipped', {
      result: 'shipped',
      insights: { engine: 'codex' },
    });
    expect(getEngineBreakerSnapshot()).toContainEqual(
      expect.objectContaining({ factoryId: 'fac_1', engine: 'codex', failureCount: 0 })
    );
  });

  it('when enabled, an OPEN (factory, engine) breaker is NOT routed that engine', async () => {
    process.env.FLEET_ENGINE_BREAKER = '1';
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex'); // → OPEN (default threshold 3)
    await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    expect(claim).toBeNull();
  });

  it('the breaker routes AROUND a broken engine to a healthy candidate', async () => {
    process.env.FLEET_ENGINE_BREAKER = '1';
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex'); // codex OPEN on fac_1
    await coord.submitJob(PID, input({ idempotencyKey: 'c', engine: 'codex' }));
    await coord.submitJob(PID, input({ idempotencyKey: 'd', engine: 'devin' }));
    const claim = await coord.claimNextJob(
      factory({ capabilities: ['engine:codex', 'engine:devin'] })
    );
    expect(claim?.job.engine).toBe('devin');
  });

  it('with the flag OFF, an OPEN breaker does NOT restrict routing (default behavior)', async () => {
    delete process.env.FLEET_ENGINE_BREAKER;
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex');
    engineBreaker.recordFailure('fac_1', 'codex');
    await coord.submitJob(PID, input({ engine: 'codex' }));
    const claim = await coord.claimNextJob(factory({ capabilities: ['engine:codex'] }));
    expect(claim?.job.engine).toBe('codex');
  });
});

describe('fleet coordinator §2 — cost/latency-aware routing', () => {
  beforeEach(() => {
    setProvider(new MemoryDatastoreProvider());
    engineBreaker.clear();
  });
  afterEach(() => {
    _resetDatastoreProvider();
    delete process.env.FLEET_COST_ROUTING;
  });

  /** Seed two completed runs so devin reads as cheap+fast and codex as pricey+slow. */
  async function seedHistory(): Promise<void> {
    const t0 = Date.parse('2026-06-01T00:00:00.000Z');
    await repo.createRun({
      id: 'seed-devin',
      productId: PID,
      jobId: 'seed-devin-job',
      attempt: 1,
      factoryId: 'fac_1',
      engine: 'devin',
      startedAt: new Date(t0).toISOString(),
      endedAt: new Date(t0 + 1000).toISOString(),
      insights: { engine: 'devin', costUsd: 1 },
    });
    await repo.createRun({
      id: 'seed-codex',
      productId: PID,
      jobId: 'seed-codex-job',
      attempt: 1,
      factoryId: 'fac_1',
      engine: 'codex',
      startedAt: new Date(t0).toISOString(),
      endedAt: new Date(t0 + 8000).toISOString(),
      insights: { engine: 'codex', costUsd: 8 },
    });
  }

  it('prefers the historically cheaper/faster engine over the older candidate', async () => {
    await seedHistory();
    // codex submitted FIRST (older) — without cost routing the age tie-break picks it.
    await coord.submitJob(PID, input({ idempotencyKey: 'codex-job', engine: 'codex' }));
    await sleep(3);
    await coord.submitJob(PID, input({ idempotencyKey: 'devin-job', engine: 'devin' }));
    const both = factory({ capabilities: ['engine:codex', 'engine:devin'] });

    process.env.FLEET_COST_ROUTING = '1';
    const claim = await coord.claimNextJob(both);
    expect(claim?.job.engine).toBe('devin'); // cheaper engine wins the nudge
  });

  it('with the flag OFF, the age tie-break stands (older codex wins) — default behavior', async () => {
    await seedHistory();
    await coord.submitJob(PID, input({ idempotencyKey: 'codex-job', engine: 'codex' }));
    await sleep(3);
    await coord.submitJob(PID, input({ idempotencyKey: 'devin-job', engine: 'devin' }));
    const both = factory({ capabilities: ['engine:codex', 'engine:devin'] });

    delete process.env.FLEET_COST_ROUTING;
    const claim = await coord.claimNextJob(both);
    expect(claim?.job.engine).toBe('codex'); // older job wins without the nudge
  });
});
