"use strict";
/**
 * test_queue_runner_lifecycle — Phase 1 (Daemon Wakeup) regression suite.
 *
 * Five scenarios:
 *   1. QueueRunner start → enqueue → poll → mission completes (mock executor).
 *   2. stop() drains in-flight missions (awaits the running promise).
 *   3. stop() honors the drain timeout — a slow mission past the deadline does
 *      not block the runner from returning.
 *   4. Non-retryable failure (retryable:false) goes terminal immediately,
 *      bypassing maxAttempts retry.
 *   5. OrchestratedMissionExecutor.judgeOutcome — a mission blocked by a
 *      CRITICAL governance event is reported failed + non-retryable.
 *
 * Real Brain/TaskExecutor are never constructed (scheduler/loop injected).
 *
 * Prints TEST_QUEUE_RUNNER_LIFECYCLE_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 QueueRunner_1 = require("./orchestrator/QueueRunner");
const DistributedLock_1 = require("./orchestrator/util/DistributedLock");
const MissionExecutor_1 = require("./orchestrator/MissionExecutor");
function assert(cond, msg) {
    if (!cond)
        throw new Error(`assertion failed: ${msg}`);
}
function mkTmp() { return fs.mkdtempSync(path.join(os.tmpdir(), 'qr-test-')); }
function rmTmp(d) { try {
    fs.rmSync(d, { recursive: true, force: true });
}
catch { /* */ } }
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, 15));
    }
    throw new Error('waitFor timed out');
}
class MockExecutor {
    delayMs;
    resultFor;
    seen = [];
    constructor(delayMs = 10, resultFor = () => ({ status: 'completed' })) {
        this.delayMs = delayMs;
        this.resultFor = resultFor;
    }
    async execute(m) {
        this.seen.push(m.id);
        await new Promise((r) => setTimeout(r, this.delayMs));
        return this.resultFor(m.id);
    }
}
function makeScheduler(tmp, executor, maxAttempts = 5) {
    return new QueueScheduler_1.QueueScheduler({
        queueFile: path.join(tmp, '.openlife', 'agent-queue.json'),
        // Isolate locks per test. Without this the scheduler's DistributedLock
        // defaults to the repo-global <cwd>/.openlife/locks/, so a mission whose
        // detached run outlives a short drainTimeout (scenario 3) leaves a zombie
        // lock that blocks the same mission id on the next standalone run.
        lock: new DistributedLock_1.DistributedLock({ baseDir: path.join(tmp, '.openlife', 'locks') }),
        pollIntervalMs: 100_000, // manual tick
        maxConcurrent: 3,
        executor,
        maxAttempts,
    });
}
// ── Scenario 1 — runner lifecycle happy path ─────────────────────
async function scenarioLifecycle() {
    const tmp = mkTmp();
    try {
        const exec = new MockExecutor(10);
        const sched = makeScheduler(tmp, exec);
        const runner = new QueueRunner_1.QueueRunner({ scheduler: sched });
        runner.enqueue({ id: 'm-1', goal: 'do thing' });
        assert(!runner.isRunning(), 'runner not started yet');
        runner.start();
        assert(runner.isRunning(), 'runner started');
        await waitFor(() => runner.list().every((m) => m.status === 'completed'));
        assert(exec.seen.includes('m-1'), 'mission executed');
        await runner.stop();
        assert(!runner.isRunning(), 'runner stopped');
        console.log('✅ scenario 1: QueueRunner start → enqueue → poll → completed → stop');
    }
    finally {
        rmTmp(tmp);
    }
}
// ── Scenario 2 — stop drains in-flight ───────────────────────────
async function scenarioDrain() {
    const tmp = mkTmp();
    try {
        let finished = false;
        const exec = new MockExecutor(150, () => { finished = true; return { status: 'completed' }; });
        const sched = makeScheduler(tmp, exec);
        const runner = new QueueRunner_1.QueueRunner({ scheduler: sched, drainTimeoutMs: 2_000 });
        runner.enqueue({ id: 'm-slow', goal: 'slow mission' });
        runner.start();
        await waitFor(() => runner.inFlightCount() === 1, 1_000);
        // stop() with default drain should wait for the in-flight mission to finish
        await runner.stop();
        assert(finished, 'in-flight mission finished before stop returned (drain)');
        assert(runner.inFlightCount() === 0, 'no in-flight after drain');
        console.log('✅ scenario 2: stop() drains in-flight missions');
    }
    finally {
        rmTmp(tmp);
    }
}
// ── Scenario 3 — drain timeout does not block forever ────────────
async function scenarioDrainTimeout() {
    const tmp = mkTmp();
    try {
        const exec = new MockExecutor(5_000); // longer than drain timeout
        const sched = makeScheduler(tmp, exec);
        const runner = new QueueRunner_1.QueueRunner({ scheduler: sched, drainTimeoutMs: 200 });
        runner.enqueue({ id: 'm-stuck', goal: 'never finishes in time' });
        runner.start();
        await waitFor(() => runner.inFlightCount() === 1, 1_000);
        const t0 = Date.now();
        await runner.stop();
        const elapsed = Date.now() - t0;
        assert(elapsed < 1_500, `stop returned within drain timeout window, took ${elapsed}ms`);
        console.log(`✅ scenario 3: drain timeout honored (stop returned in ${elapsed}ms)`);
    }
    finally {
        rmTmp(tmp);
    }
}
// ── Scenario 4 — non-retryable failure goes terminal ─────────────
async function scenarioNonRetryable() {
    const tmp = mkTmp();
    try {
        const exec = new MockExecutor(5, () => ({ status: 'failed', retryable: false, error: 'governance_blocked: scope_violation' }));
        const sched = makeScheduler(tmp, exec, /* maxAttempts */ 5);
        sched.enqueue({ id: 'm-blocked', goal: 'do forbidden thing' });
        await sched.tick();
        await waitFor(() => sched.list().find((m) => m.id === 'm-blocked')?.status === 'failed');
        const mission = sched.list().find((m) => m.id === 'm-blocked');
        assert(mission?.status === 'failed', `expected failed, got ${mission?.status}`);
        assert(mission?.attempts === 1, `non-retryable should fail after 1 attempt, got ${mission?.attempts}`);
        console.log('✅ scenario 4: non-retryable failure goes terminal (no retry)');
    }
    finally {
        rmTmp(tmp);
    }
}
// ── Scenario 5 — judgeOutcome maps governance block ──────────────
function makeMissionState(status, governanceEvents) {
    const now = new Date().toISOString();
    return {
        taskId: 't1', goal: 'g', userId: 'u', status,
        plan: [], successCriteria: [], attempts: [], reviewFindings: [],
        artifacts: [], governanceEvents, createdAt: now, updatedAt: now,
    };
}
async function scenarioJudgeOutcome() {
    // Inject a fake OrchestrationLoop that returns a governance-blocked state.
    const blockedState = makeMissionState('failed', [{
            type: 'scope_violation', level: 'critical',
            summary: 'goal exceeds allowed scope', at: new Date().toISOString(),
        }]);
    const fakeLoop = {
        // eslint-disable-next-line @typescript-eslint/require-await
        run: async () => ({ state: blockedState, response: 'blocked', missionPath: '/tmp/x' }),
    };
    const executor = new MissionExecutor_1.OrchestratedMissionExecutor({
        // brain/taskExecutor unused because loop is injected
        brain: {},
        taskExecutor: {},
        loop: fakeLoop,
    });
    // NOTE: goal is benign on purpose. This scenario exercises judgeOutcome's
    // mapping of a governance-blocked loop STATE → non-retryable failure. The
    // Phase 3 MissionGovernor gate runs first; a dangerous goal (e.g. 'rm -rf /')
    // would be blocked there with `mission_blocked_by_governance:` before the
    // injected loop ever runs, short-circuiting what this scenario tests.
    const res = await executor.execute({
        id: 'm-gov', goal: 'summarize the changelog', status: 'pending', attempts: 0, enqueuedAt: new Date().toISOString(),
    });
    assert(res.status === 'failed', `expected failed, got ${res.status}`);
    assert(res.retryable === false, `governance block must be non-retryable, got ${res.retryable}`);
    assert(res.error?.startsWith('governance_blocked:'), `expected governance_blocked error, got ${res.error}`);
    // Sanity: a completed state maps to success.
    const okState = makeMissionState('completed', []);
    const fakeOkLoop = {
        // eslint-disable-next-line @typescript-eslint/require-await
        run: async () => ({ state: okState, response: 'done', missionPath: '/tmp/y' }),
    };
    const okExec = new MissionExecutor_1.OrchestratedMissionExecutor({ brain: {}, taskExecutor: {}, loop: fakeOkLoop });
    const okRes = await okExec.execute({ id: 'm-ok', goal: 'echo hi', status: 'pending', attempts: 0, enqueuedAt: new Date().toISOString() });
    assert(okRes.status === 'completed', `completed state maps to success, got ${okRes.status}`);
    console.log('✅ scenario 5: judgeOutcome — governance block → non-retryable; completed → success');
}
async function main() {
    console.log('🧪 test_queue_runner_lifecycle — Phase 1 (Daemon Wakeup)');
    await scenarioLifecycle();
    await scenarioDrain();
    await scenarioDrainTimeout();
    await scenarioNonRetryable();
    await scenarioJudgeOutcome();
    console.log('');
    console.log('TEST_QUEUE_RUNNER_LIFECYCLE_OK');
}
main().catch((err) => {
    console.error('❌ test_queue_runner_lifecycle FAILED:', err instanceof Error ? err.message : err);
    process.exit(1);
});
