"use strict";
/**
 * test_queue_scheduler — Story 6.3 regression suite.
 *
 * Four scenarios:
 *   1. enqueue + tick + happy path: missions transition pending→running→
 *      completed; queue file persists each transition atomically.
 *   2. Backpressure: maxConcurrent caps the in-flight set; extras wait.
 *   3. Lock-respect: a mission whose lock is already held by another
 *      DistributedLock instance is NOT picked up.
 *   4. reconcileOrphans: a queue with stale `running` items (no holder)
 *      resets them to `pending` on the next start.
 *
 * Prints TEST_QUEUE_SCHEDULER_OK on full pass.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const QueueScheduler_1 = require("./orchestrator/QueueScheduler");
const DistributedLock_1 = require("./orchestrator/util/DistributedLock");
const AtomicWriter_1 = require("./orchestrator/util/AtomicWriter");
function assert(cond, msg) {
    if (!cond)
        throw new Error(`assertion failed: ${msg}`);
}
function mkTmp() {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'qs-test-'));
}
function rmTmp(dir) {
    try {
        fs.rmSync(dir, { recursive: true, force: true });
    }
    catch { /* ignore */ }
}
class CountingExecutor {
    delayMs;
    failFor;
    seen = [];
    concurrentPeak = 0;
    inFlight = 0;
    constructor(delayMs = 20, failFor = new Set()) {
        this.delayMs = delayMs;
        this.failFor = failFor;
    }
    async execute(m) {
        this.inFlight += 1;
        this.concurrentPeak = Math.max(this.concurrentPeak, this.inFlight);
        this.seen.push(m.id);
        await new Promise((r) => setTimeout(r, this.delayMs));
        this.inFlight -= 1;
        if (this.failFor.has(m.id))
            return { status: 'failed', error: 'scripted failure' };
        return { status: 'completed' };
    }
}
function makeScheduler(tmp, opts = {}) {
    const queueFile = path.join(tmp, '.openlife', 'agent-queue.json');
    return new QueueScheduler_1.QueueScheduler({
        queueFile,
        pollIntervalMs: 100_000, // we'll call tick() manually
        maxConcurrent: opts.maxConcurrent ?? 3,
        executor: opts.executor,
        lock: opts.lock,
        maxAttempts: opts.maxAttempts,
    });
}
async function waitFor(predicate, timeoutMs = 2_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        if (predicate())
            return;
        await new Promise((r) => setTimeout(r, 20));
    }
    throw new Error('waitFor timed out');
}
// ─────────────────────────────────────────────────────────────
// Scenario 1 — happy path
// ─────────────────────────────────────────────────────────────
async function scenarioHappyPath() {
    const tmp = mkTmp();
    try {
        const exec = new CountingExecutor(10);
        const sched = makeScheduler(tmp, { executor: exec, maxConcurrent: 3 });
        sched.enqueue({ id: 'm-1', goal: 'do thing 1' });
        sched.enqueue({ id: 'm-2', goal: 'do thing 2' });
        sched.enqueue({ id: 'm-3', goal: 'do thing 3' });
        await sched.tick();
        // Wait for all to complete
        await waitFor(() => sched.list().every((m) => m.status === 'completed'));
        const items = sched.list();
        assert(items.length === 3, `expected 3 items, got ${items.length}`);
        assert(items.every((m) => m.status === 'completed'), 'all must be completed');
        assert(exec.seen.length === 3, `executor must have seen 3, got ${exec.seen.length}`);
        console.log('✅ scenario 1: enqueue + happy path (3 missions completed)');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 2 — backpressure
// ─────────────────────────────────────────────────────────────
async function scenarioBackpressure() {
    const tmp = mkTmp();
    try {
        const exec = new CountingExecutor(80); // each takes ~80ms
        const sched = makeScheduler(tmp, { executor: exec, maxConcurrent: 2 });
        for (let i = 0; i < 5; i++)
            sched.enqueue({ id: `m-${i}`, goal: `goal ${i}` });
        // First tick: picks 2; others stay pending
        await sched.tick();
        // While the first 2 are still in flight, peak should be 2
        await new Promise((r) => setTimeout(r, 40));
        assert(exec.concurrentPeak <= 2, `concurrent peak must respect maxConcurrent, got ${exec.concurrentPeak}`);
        // Wait for first 2 to complete, then tick again
        await waitFor(() => sched.list().filter((m) => m.status === 'completed').length >= 2);
        await sched.tick();
        await waitFor(() => sched.list().filter((m) => m.status === 'completed').length >= 4);
        await sched.tick();
        await waitFor(() => sched.list().every((m) => m.status === 'completed'));
        assert(exec.concurrentPeak <= 2, `final peak must not exceed maxConcurrent, got ${exec.concurrentPeak}`);
        console.log(`✅ scenario 2: backpressure (peak ${exec.concurrentPeak} ≤ maxConcurrent=2)`);
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 3 — lock-respect (mission lock pre-held)
// ─────────────────────────────────────────────────────────────
async function scenarioLockRespect() {
    const tmp = mkTmp();
    try {
        const baseDir = path.join(tmp, '.openlife', 'locks');
        const externalLock = new DistributedLock_1.DistributedLock({ baseDir });
        const sharedLock = new DistributedLock_1.DistributedLock({ baseDir });
        // Pre-hold the lock for mission m-locked
        const acq = externalLock.acquire('mission:m-locked', 60_000, { startHeartbeat: false });
        assert(acq.ok, 'external acquire must succeed');
        const exec = new CountingExecutor(10);
        const sched = makeScheduler(tmp, { executor: exec, maxConcurrent: 5, lock: sharedLock });
        sched.enqueue({ id: 'm-locked', goal: 'should be skipped' });
        sched.enqueue({ id: 'm-free', goal: 'should run' });
        await sched.tick();
        await waitFor(() => sched.list().find((m) => m.id === 'm-free')?.status === 'completed');
        const locked = sched.list().find((m) => m.id === 'm-locked');
        assert(locked?.status === 'pending', `locked mission must stay pending, got ${locked?.status}`);
        assert(exec.seen.includes('m-free'), 'free mission must have executed');
        assert(!exec.seen.includes('m-locked'), 'locked mission must NOT have executed');
        // Release external lock — next tick should now pick it up
        externalLock.release(acq.lockId);
        await sched.tick();
        await waitFor(() => sched.list().find((m) => m.id === 'm-locked')?.status === 'completed');
        console.log('✅ scenario 3: lock-respect (locked mission skipped, picked up after release)');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 4 — reconcile orphans
// ─────────────────────────────────────────────────────────────
async function scenarioReconcileOrphans() {
    const tmp = mkTmp();
    try {
        const queueFile = path.join(tmp, '.openlife', 'agent-queue.json');
        // Seed queue with a "running" orphan (simulates a crash mid-flight)
        (0, AtomicWriter_1.writeJsonAtomic)(queueFile, {
            items: [
                { id: 'm-orphan', goal: 'crashed last run', status: 'running', enqueuedAt: new Date().toISOString(), attempts: 1, startedAt: new Date(Date.now() - 60_000).toISOString() },
                { id: 'm-fresh', goal: 'never started', status: 'pending', enqueuedAt: new Date().toISOString(), attempts: 0 },
            ],
            lastFlushedAt: new Date().toISOString(),
        });
        const exec = new CountingExecutor(10);
        const sched = makeScheduler(tmp, { executor: exec });
        const resetCount = sched.reconcileOrphans();
        assert(resetCount === 1, `expected 1 orphan reset, got ${resetCount}`);
        assert(sched.list().find((m) => m.id === 'm-orphan')?.status === 'pending', 'orphan must be reset to pending');
        // Now tick — both should complete
        await sched.tick();
        await waitFor(() => sched.list().every((m) => m.status === 'completed'));
        assert(exec.seen.includes('m-orphan') && exec.seen.includes('m-fresh'), 'both must execute after reconcile');
        console.log('✅ scenario 4: reconcileOrphans resets stale running missions');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Driver
// ─────────────────────────────────────────────────────────────
async function main() {
    console.log('🧪 test_queue_scheduler — Story 6.3 regression suite');
    await scenarioHappyPath();
    await scenarioBackpressure();
    await scenarioLockRespect();
    await scenarioReconcileOrphans();
    console.log('');
    console.log('TEST_QUEUE_SCHEDULER_OK');
}
main().catch((err) => {
    console.error('❌ test_queue_scheduler FAILED:', err instanceof Error ? err.message : err);
    process.exit(1);
});
