"use strict";
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 });
/*
 * Story 3.7 — Multi-host installer E2E (spawned CLI) regression test.
 *
 * The unit tests for HostInstaller (src/test_host_installer.ts) and host
 * validation (src/test_install_flow_host_validation.ts) cover the classes in
 * isolation. This test SPAWNS `node bin/openlife.js` against a temp dir to
 * validate the full stack: Commander flag parsing, lazy require chain, JSON
 * output, exit codes, and side effects on disk.
 *
 * Scenarios:
 *  1. claude-code install + status + uninstall full cycle
 *  2. gemini-cli host degrades gracefully (state created, host install skipped)
 *  3. codex host degrades gracefully (same as #2)
 *  4. invalid host (e.g. 'cursor') yields a clear error
 *  5. uninstall in a clean project is idempotent (all skipped-not-found)
 *  6. reinstall is idempotent (no duplicated agent files on disk)
 *
 * NOTE on timeouts: CLI cold start is ~5-10s in WSL due to the require tree
 * (Brain, Gateway, etc. — even though most are lazy in command handlers, some
 * top-level imports still resolve). Each spawn gets a 60s shell timeout which
 * is comfortably above observed worst-case.
 */
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const CLI = path.join(process.cwd(), 'dist', 'index.js');
const SPAWN_TIMEOUT_MS = Number(process.env.OPENLIFE_TEST_E2E_TIMEOUT_MS || 60000);
function assert(cond, msg) {
    if (!cond)
        throw new Error(`ASSERTION_FAILED: ${msg}`);
}
function mkTemp() {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-e2e-test-'));
}
function runCli(args, cwd, timeoutMs = SPAWN_TIMEOUT_MS) {
    return new Promise((resolve) => {
        const t0 = Date.now();
        // Strip TELEGRAM_BOT_TOKEN and OPENAI_API_KEY so the CLI does not try to
        // bind the gateway or any real model — tests are pure filesystem ops.
        // We DO need PATH/HOME for Node.
        const childEnv = { ...process.env };
        delete childEnv.TELEGRAM_BOT_TOKEN;
        const child = (0, child_process_1.spawn)('node', [CLI, ...args], {
            cwd,
            stdio: ['ignore', 'pipe', 'pipe'],
            env: childEnv
        });
        let stdout = '';
        let stderr = '';
        child.stdout?.on('data', (d) => { stdout += d.toString(); });
        child.stderr?.on('data', (d) => { stderr += d.toString(); });
        let timedOut = false;
        const killer = setTimeout(() => {
            timedOut = true;
            child.kill('SIGKILL');
        }, timeoutMs);
        child.on('exit', (code, signal) => {
            clearTimeout(killer);
            resolve({ code, signal, stdout, stderr, elapsed: Date.now() - t0, timedOut });
        });
    });
}
// Extract the LARGEST top-level JSON object from a stdout blob. The CLI emits
// banners (installationBanner, summary lines) plus a JSON payload that itself
// contains nested objects. We want the outer envelope, so we scan for '{' at
// indentation level 0 (depth resets between scans) and pick the longest match
// whose parser succeeds.
function extractLastJsonObject(blob) {
    let best = null;
    let bestLen = 0;
    let i = 0;
    while (i < blob.length) {
        if (blob[i] !== '{') {
            i++;
            continue;
        }
        let depth = 0;
        let end = -1;
        for (let j = i; j < blob.length; j++) {
            const c = blob[j];
            if (c === '{')
                depth++;
            else if (c === '}') {
                depth--;
                if (depth === 0) {
                    end = j;
                    break;
                }
            }
        }
        if (end === -1)
            break;
        const slice = blob.slice(i, end + 1);
        try {
            const parsed = JSON.parse(slice);
            if (parsed && typeof parsed === 'object' && slice.length >= bestLen) {
                best = parsed;
                bestLen = slice.length;
            }
        }
        catch { /* ignore */ }
        // Skip past this object — nested duplicates of the same payload are
        // shorter, so they won't beat `best` once we capture the outer one.
        i = end + 1;
    }
    return best;
}
// ----- Scenario 1: claude-code install + status + uninstall full cycle -----
async function scenario1_claudeCodeFullCycle() {
    const tempRoot = mkTemp();
    try {
        // 1. system setup --host claude-code
        const installRes = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'claude-code', '--skip-doctor'], tempRoot);
        if (installRes.timedOut) {
            throw new Error(`SCENARIO1_INSTALL_TIMEOUT after ${installRes.elapsed}ms`);
        }
        assert(installRes.code === 0, `scenario1: setup should exit 0; got code=${installRes.code}, stderr=${installRes.stderr.slice(0, 400)}`);
        // Side effects on disk
        const manifestPath = path.join(tempRoot, '.openlife', 'install-manifest.json');
        assert(fs.existsSync(manifestPath), `scenario1: install-manifest.json missing at ${manifestPath}`);
        const maestro = path.join(tempRoot, '.claude', 'agents', 'openlife-maestro.md');
        assert(fs.existsSync(maestro), `scenario1: openlife-maestro.md missing at ${maestro}`);
        const statusCmd = path.join(tempRoot, '.claude', 'commands', 'openlife', 'status.md');
        assert(fs.existsSync(statusCmd), `scenario1: commands/openlife/status.md missing at ${statusCmd}`);
        const mcpSnippet = path.join(tempRoot, '.openlife', 'install-mcp-snippet.json');
        // MCP snippet is gated behind OPENLIFE_PREVIEW_MCP=1 — should be absent by default.
        assert(!fs.existsSync(mcpSnippet), `scenario1: install-mcp-snippet.json should NOT exist by default (gated by OPENLIFE_PREVIEW_MCP=1)`);
        // 2. system status — should reflect tempRoot via process.cwd()
        const statusRes = await runCli(['system', 'status'], tempRoot);
        if (statusRes.timedOut)
            throw new Error(`SCENARIO1_STATUS_TIMEOUT after ${statusRes.elapsed}ms`);
        assert(statusRes.code === 0, `scenario1: status should exit 0; got ${statusRes.code}`);
        const statusJson = extractLastJsonObject(statusRes.stdout);
        assert(statusJson !== null, `scenario1: status should emit JSON; got: ${statusRes.stdout.slice(0, 400)}`);
        assert(statusJson.hasInstallManifest === true, `scenario1: status.hasInstallManifest should be true; got ${JSON.stringify(statusJson)}`);
        // 3. system uninstall --host claude-code
        const uninstallRes = await runCli(['system', 'uninstall', '--host', 'claude-code'], tempRoot);
        if (uninstallRes.timedOut)
            throw new Error(`SCENARIO1_UNINSTALL_TIMEOUT after ${uninstallRes.elapsed}ms`);
        assert(uninstallRes.code === 0, `scenario1: uninstall should exit 0; got ${uninstallRes.code}, stderr=${uninstallRes.stderr.slice(0, 400)}`);
        // Host files should be gone…
        assert(!fs.existsSync(maestro), `scenario1: openlife-maestro.md should be removed; still at ${maestro}`);
        // commands/openlife may either be gone or empty — the unit test asserts directory is removed when empty.
        const commandsDir = path.join(tempRoot, '.claude', 'commands', 'openlife');
        if (fs.existsSync(commandsDir)) {
            const remaining = fs.readdirSync(commandsDir);
            assert(remaining.length === 0, `scenario1: commands/openlife should be empty or removed; still contains: ${remaining.join(',')}`);
        }
        assert(!fs.existsSync(mcpSnippet), `scenario1: install-mcp-snippet.json should be removed; still at ${mcpSnippet}`);
        // …but .openlife state must be preserved (manifest survives).
        assert(fs.existsSync(manifestPath), `scenario1: install-manifest.json should be preserved after uninstall; missing at ${manifestPath}`);
        assert(fs.existsSync(path.join(tempRoot, '.openlife')), `scenario1: .openlife dir should be preserved after uninstall`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
// ----- Scenario 2: gemini-cli degrades gracefully -----
async function scenario2_geminiCliDegrades() {
    const tempRoot = mkTemp();
    try {
        const res = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'gemini-cli', '--skip-doctor'], tempRoot);
        if (res.timedOut)
            throw new Error(`SCENARIO2_TIMEOUT after ${res.elapsed}ms`);
        assert(res.code === 0, `scenario2: setup should exit 0 even for unsupported host; got ${res.code}, stderr=${res.stderr.slice(0, 400)}`);
        // .openlife state created
        const manifestPath = path.join(tempRoot, '.openlife', 'install-manifest.json');
        assert(fs.existsSync(manifestPath), `scenario2: install-manifest.json should still be created; missing at ${manifestPath}`);
        // .claude/ should NOT exist (no host install for gemini-cli)
        const claudeDir = path.join(tempRoot, '.claude');
        assert(!fs.existsSync(claudeDir), `scenario2: .claude/ should NOT be created for gemini-cli; found at ${claudeDir}`);
        // Output should hint at the graceful skip somewhere
        const combined = (res.stdout + '\n' + res.stderr).toLowerCase();
        assert(combined.includes('skipped') ||
            combined.includes('host_not_yet_supported') ||
            combined.includes('not yet supported') ||
            combined.includes('host install'), `scenario2: output should mention skip/unsupported; got stdout=${res.stdout.slice(0, 600)}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
// ----- Scenario 3: codex degrades gracefully (same shape as #2) -----
async function scenario3_codexDegrades() {
    const tempRoot = mkTemp();
    try {
        const res = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'codex', '--skip-doctor'], tempRoot);
        if (res.timedOut)
            throw new Error(`SCENARIO3_TIMEOUT after ${res.elapsed}ms`);
        assert(res.code === 0, `scenario3: setup should exit 0 even for unsupported host; got ${res.code}, stderr=${res.stderr.slice(0, 400)}`);
        const manifestPath = path.join(tempRoot, '.openlife', 'install-manifest.json');
        assert(fs.existsSync(manifestPath), `scenario3: install-manifest.json missing at ${manifestPath}`);
        const claudeDir = path.join(tempRoot, '.claude');
        assert(!fs.existsSync(claudeDir), `scenario3: .claude/ should NOT be created for codex; found at ${claudeDir}`);
        const combined = (res.stdout + '\n' + res.stderr).toLowerCase();
        assert(combined.includes('skipped') ||
            combined.includes('host_not_yet_supported') ||
            combined.includes('not yet supported') ||
            combined.includes('host install'), `scenario3: output should mention skip/unsupported; got stdout=${res.stdout.slice(0, 600)}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
// ----- Scenario 4: invalid host returns clear error -----
async function scenario4_invalidHostFails() {
    const tempRoot = mkTemp();
    try {
        const res = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'cursor', '--skip-doctor'], tempRoot);
        if (res.timedOut)
            throw new Error(`SCENARIO4_TIMEOUT after ${res.elapsed}ms`);
        // Story 3.1: invalid host must throw INVALID_HOST. InstallFlow.run() throws,
        // which bubbles up unhandled in the action handler. The process should exit
        // non-zero (either via thrown error or signal) and/or surface a clear msg.
        const combined = (res.stdout + '\n' + res.stderr).toLowerCase();
        const mentionsInvalid = combined.includes('invalid_host') || combined.includes('invalid host');
        const mentionsCursor = combined.includes('cursor');
        const mentionsValidHosts = combined.includes('claude-code') && combined.includes('gemini-cli');
        assert(mentionsInvalid || (mentionsCursor && mentionsValidHosts), `scenario4: output should clearly identify invalid host 'cursor'; got stdout=${res.stdout.slice(0, 400)} stderr=${res.stderr.slice(0, 400)}`);
        assert(res.code !== 0, `scenario4: invalid host should yield non-zero exit; got code=${res.code}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
// ----- Scenario 5: uninstall on clean project is idempotent -----
async function scenario5_uninstallOnCleanProject() {
    const tempRoot = mkTemp();
    try {
        const res = await runCli(['system', 'uninstall', '--host', 'claude-code'], tempRoot);
        if (res.timedOut)
            throw new Error(`SCENARIO5_TIMEOUT after ${res.elapsed}ms`);
        assert(res.code === 0, `scenario5: uninstall on clean project should exit 0; got ${res.code}, stderr=${res.stderr.slice(0, 400)}`);
        const json = extractLastJsonObject(res.stdout);
        assert(json !== null, `scenario5: uninstall should emit JSON; got: ${res.stdout.slice(0, 400)}`);
        assert(json.ok === true, `scenario5: uninstall should report ok=true; got ${JSON.stringify(json)}`);
        // Every file action should be 'skipped-not-found' on a clean dir.
        const files = Array.isArray(json.files) ? json.files : [];
        assert(files.length > 0, `scenario5: uninstall should report at least one file action; got ${JSON.stringify(json)}`);
        const allSkipped = files.every((f) => f.action === 'skipped-not-found');
        assert(allSkipped, `scenario5: all actions should be 'skipped-not-found' on clean dir; got ${JSON.stringify(files.map((f) => f.action))}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
// ----- Scenario 6: reinstall is idempotent (no duplicate agents) -----
async function scenario6_reinstallIdempotent() {
    const tempRoot = mkTemp();
    try {
        // First install
        const r1 = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'claude-code', '--skip-doctor'], tempRoot);
        if (r1.timedOut)
            throw new Error(`SCENARIO6_INSTALL1_TIMEOUT after ${r1.elapsed}ms`);
        assert(r1.code === 0, `scenario6: first setup should exit 0; got ${r1.code}`);
        const agentsDir = path.join(tempRoot, '.claude', 'agents');
        const before = fs.readdirSync(agentsDir).filter((f) => f.startsWith('openlife-') && f.endsWith('.md')).sort();
        assert(before.length > 0, `scenario6: should have openlife-*.md agents after first install`);
        // Second install — should be idempotent
        const r2 = await runCli(['system', 'setup', '--profile', 'framework', '--host', 'claude-code', '--skip-doctor'], tempRoot);
        if (r2.timedOut)
            throw new Error(`SCENARIO6_INSTALL2_TIMEOUT after ${r2.elapsed}ms`);
        assert(r2.code === 0, `scenario6: second setup should exit 0; got ${r2.code}`);
        const after = fs.readdirSync(agentsDir).filter((f) => f.startsWith('openlife-') && f.endsWith('.md')).sort();
        assert(after.length === before.length, `scenario6: reinstall should NOT duplicate agents; before=${before.length} (${before.join(',')}) after=${after.length} (${after.join(',')})`);
        assert(JSON.stringify(before) === JSON.stringify(after), `scenario6: agent set should be identical before/after reinstall; before=${JSON.stringify(before)} after=${JSON.stringify(after)}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
async function main() {
    if (!fs.existsSync(CLI)) {
        throw new Error(`MISSING_BUILD: ${CLI} — run 'npm run build' first`);
    }
    const t0 = Date.now();
    await scenario1_claudeCodeFullCycle();
    await scenario2_geminiCliDegrades();
    await scenario3_codexDegrades();
    await scenario4_invalidHostFails();
    await scenario5_uninstallOnCleanProject();
    await scenario6_reinstallIdempotent();
    const elapsed = Date.now() - t0;
    console.log(`TEST_HOST_INSTALL_E2E_OK (elapsed=${elapsed}ms, scenarios=6)`);
}
main().catch((err) => {
    console.error('TEST_HOST_INSTALL_E2E_FAIL:', err?.message || err);
    process.exit(1);
});
