import { randomBytes } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { LocalSqliteStore } from "../persistence/LocalSqliteStore.js";
import { startBridge } from "./opencodeRuntimeSmoke.js";
import { delay } from "./smokeTiming.js";
const DEFAULT_TIMEOUT_MS = 30_000;
const WORKSPACE_ID = "runtime-timeline-smoke-workspace";
const RUN_ID = "run_runtime_timeline_smoke";
const RUNTIME_SESSION_ID = "runtime-session-timeline-forbidden";
const LOCAL_UI_ORIGIN = "http://127.0.0.1:41732";
const LOCAL_UI_DEVICE_ID = "runtime-timeline-smoke-device";
const LOCAL_UI_USER_SESSION_ID = "runtime-timeline-smoke-user-session";
const REQUIRED_LOCAL_UI_SCOPES = ["bridge:read"];
const MISSING_SCOPE_LOCAL_UI_SCOPES = ["permissions:reply"];
const PROOFED_LOCAL_UI_ROUTES = [
    "POST /local-ui/capabilities",
    "POST /local-ui/session",
    "GET /local-ui/session",
    "GET /runs/:runId/events",
];
const PERSISTENCE_LABELS = [
    "initial-read",
    "refresh-read",
    "restart-read",
];
const UNAVAILABLE_RAW_FIELDS = [
    "runtimeSessionId",
    "providerRequestId",
    "providerPermissionId",
    "messageId",
    "toolCallId",
    "command",
    "commandMatchKey",
    "matcherKey",
    "localPath",
    "promptText",
    "providerId",
    "modelId",
    "rawPayload",
    "fakeSecret",
];
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const HOSTILE_PROVIDER_REQUEST_ID = "provider-request-timeline-forbidden";
const HOSTILE_PROVIDER_PERMISSION_ID = "provider-permission-timeline-forbidden";
const HOSTILE_MESSAGE_ID = "message-timeline-forbidden";
const HOSTILE_TOOL_CALL_ID = "tool-call-timeline-forbidden";
const HOSTILE_MATCHER_KEY = "matcher-key-runtime-timeline-forbidden";
const HOSTILE_PROMPT_TEXT = "Summarize the private launch plan and API key.";
const HOSTILE_PROVIDER_ID = "provider-runtime-timeline-forbidden";
const HOSTILE_MODEL_ID = "model-runtime-timeline-forbidden";
const HOSTILE_FAKE_SECRET = "sk-runtime-timeline-fake-secret-123456";
const HOSTILE_RAW_PAYLOAD = "raw-opencode-runtime-payload-forbidden";
const HOSTILE_RAW_COMMAND = `curl https://runtime-timeline.example.invalid/deploy --api-key ${HOSTILE_FAKE_SECRET}`;
const EXPECTED_RUNTIME_TIMELINE_EVENTS = [
    {
        id: "rte_runtime_timeline_permission",
        source: "runtime",
        type: "runtime_permission",
        at: "2026-02-03T04:05:06.000Z",
        title: "Runtime permission activity",
        description: "The runtime reported permission activity.",
        tone: "warning",
        metadata: { category: "permission", status: "requested", risk: "high" },
    },
    {
        id: "rte_runtime_timeline_message",
        source: "runtime",
        type: "runtime_message",
        at: "2026-02-03T04:05:07.000Z",
        title: "Runtime message activity",
        description: "The runtime reported message activity.",
        tone: "success",
        metadata: { category: "message", status: "completed" },
    },
];
const LOCAL_UI_TOKEN_PATTERNS = [
    {
        label: "local UI capability token",
        pattern: /\bpt_lui_(?!sess_)[A-Za-z0-9_-]{16,}\b/u,
    },
    {
        label: "local UI session token",
        pattern: /\bpt_lui_sess_[A-Za-z0-9_-]{16,}\b/u,
    },
    {
        label: "local UI handoff fragment",
        pattern: /patchTrailLocalHandoff=[^\s"']+/u,
    },
];
const SECRET_PATTERNS = [
    {
        label: "OpenAI-style API key",
        pattern: /\bsk-[A-Za-z0-9_-]{6,}\b/u,
    },
];
export async function main(argv = process.argv.slice(2)) {
    const wantsJson = argv.includes("--json");
    try {
        const options = parseRuntimeTimelineSmokeArgs(argv);
        if (options.help) {
            process.stdout.write(helpText());
            return;
        }
        const summary = await runRuntimeTimelineSmoke(argv, process.env);
        if (options.json) {
            process.stdout.write(`${JSON.stringify(summary)}\n`);
            return;
        }
        process.stdout.write(`Runtime timeline smoke passed: run=${summary.runId} events=${summary.eventCount} proof=${summary.persistenceLabels.join(",")} localUiScopes=${summary.localUiScopes.join(",")}\n`);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : "Runtime timeline smoke failed.";
        process.stderr.write(wantsJson
            ? `${JSON.stringify({ status: "failed", error: message })}\n`
            : `${message}\n`);
        process.exitCode = 1;
    }
}
export async function runRuntimeTimelineSmoke(argv = [], env = process.env) {
    const options = parseRuntimeTimelineSmokeArgs(argv);
    if (options.help) {
        throw new Error("--help is only supported by the runtime timeline smoke CLI.");
    }
    return executeRuntimeTimelineSmoke(options, env);
}
export function parseRuntimeTimelineSmokeArgs(argv) {
    const options = {
        keepTemp: false,
        timeoutMs: DEFAULT_TIMEOUT_MS,
        json: false,
        help: false,
    };
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
        if (!arg || arg === "--")
            continue;
        if (arg === "--help" || arg === "-h") {
            options.help = true;
            continue;
        }
        if (arg === "--keep-temp") {
            options.keepTemp = true;
            continue;
        }
        if (arg === "--json") {
            options.json = true;
            continue;
        }
        if (arg === "--timeout-ms") {
            options.timeoutMs = parseTimeout(requireArgValue(argv, ++index, arg));
            continue;
        }
        throw new Error(`Unknown runtime timeline smoke argument: ${arg}`);
    }
    return options;
}
export function createRuntimeTimelineSmokeEnvironment(input) {
    const smokeEnv = { ...input.env };
    for (const key of [
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "GITHUB_TOKEN",
        "DATABASE_URL",
        "PATCHTRAIL_SMOKE_OPENAI_API_KEY",
        "PATCHTRAIL_CREDENTIAL_FALLBACK_KEY",
        "PATCHTRAIL_OPENCODE_METADATA_DIR",
        "PATCHTRAIL_OPENCODE_ARTIFACT_ROOT",
        "PATCHTRAIL_OPENCODE_TRUSTED_ROOT_KEY_IDS",
    ]) {
        delete smokeEnv[key];
    }
    smokeEnv.NODE_ENV = "test";
    smokeEnv.PATCHTRAIL_CREDENTIAL_STORE = "in-memory";
    smokeEnv.PATCHTRAIL_BRIDGE_HOST = "127.0.0.1";
    smokeEnv.PATCHTRAIL_BRIDGE_PORT = "0";
    smokeEnv.PATCHTRAIL_BRIDGE_CAPABILITY_TOKEN = input.bridgeToken;
    smokeEnv.PATCHTRAIL_OPENCODE_SERVER_PASSWORD = input.opencodeServerPassword;
    smokeEnv.PATCHTRAIL_APP_DATA_DIR = input.appDataDir;
    smokeEnv.PATCHTRAIL_LOCAL_DB_PATH = input.localDbPath;
    return smokeEnv;
}
export function seedRuntimeTimelineSmokeData(input) {
    const store = LocalSqliteStore.openSync(input.localDbPath);
    try {
        store.upsertWorkspace({ id: WORKSPACE_ID, rootPath: input.workspaceRoot });
        store.createRun({
            id: RUN_ID,
            workspaceId: WORKSPACE_ID,
            provider: "opencode",
            status: "running",
            title: "Runtime timeline smoke",
            runtimeSessionId: RUNTIME_SESSION_ID,
        });
        store.appendRuntimeEvent({
            id: "rte_runtime_timeline_permission",
            runId: RUN_ID,
            runtimeSessionId: RUNTIME_SESSION_ID,
            source: "opencode",
            type: "runtime_permission",
            occurredAt: "2026-02-03T04:05:06.000Z",
            title: `Permission requested for ${HOSTILE_RAW_COMMAND}`,
            description: `Provider payload ${HOSTILE_RAW_PAYLOAD} touched ${input.hostileLocalPath}`,
            tone: "destructive",
            metadata: {
                category: "permission",
                status: "requested",
                risk: "high",
                runtimeSessionId: RUNTIME_SESSION_ID,
                providerRequestId: HOSTILE_PROVIDER_REQUEST_ID,
                providerPermissionId: HOSTILE_PROVIDER_PERMISSION_ID,
                messageId: HOSTILE_MESSAGE_ID,
                toolCallId: HOSTILE_TOOL_CALL_ID,
                command: HOSTILE_RAW_COMMAND,
                commandMatchKey: HOSTILE_MATCHER_KEY,
                matcherKey: HOSTILE_MATCHER_KEY,
                localPath: input.hostileLocalPath,
                promptText: HOSTILE_PROMPT_TEXT,
                providerId: HOSTILE_PROVIDER_ID,
                modelId: HOSTILE_MODEL_ID,
                rawPayload: HOSTILE_RAW_PAYLOAD,
                fakeSecret: HOSTILE_FAKE_SECRET,
            },
        });
        store.appendRuntimeEvent({
            id: "rte_runtime_timeline_message",
            runId: RUN_ID,
            runtimeSessionId: RUNTIME_SESSION_ID,
            source: "opencode",
            type: "runtime_message",
            occurredAt: "2026-02-03T04:05:07.000Z",
            title: `Message ${HOSTILE_MESSAGE_ID} included ${HOSTILE_FAKE_SECRET}`,
            description: `Prompt ${HOSTILE_PROMPT_TEXT} used ${HOSTILE_PROVIDER_ID}/${HOSTILE_MODEL_ID}`,
            tone: "destructive",
            metadata: {
                category: "message",
                status: "completed",
                runtimeSessionId: RUNTIME_SESSION_ID,
                messageId: HOSTILE_MESSAGE_ID,
                toolCallId: HOSTILE_TOOL_CALL_ID,
                providerId: HOSTILE_PROVIDER_ID,
                modelId: HOSTILE_MODEL_ID,
                promptText: HOSTILE_PROMPT_TEXT,
                localPath: input.hostileLocalPath,
                rawPayload: HOSTILE_RAW_PAYLOAD,
                fakeSecret: HOSTILE_FAKE_SECRET,
            },
        });
        return {
            workspaceId: WORKSPACE_ID,
            runId: RUN_ID,
            runtimeSessionId: RUNTIME_SESSION_ID,
            forbidden: hostileFixtureForbiddenValues(input.hostileLocalPath),
        };
    }
    finally {
        store.close();
    }
}
export async function assertNoTimelineSmokeLeaks(surface, value, forbidden, options = {}) {
    const serialized = serializeForLeakCheck(value);
    if (options.scanLocalUiTokenPatterns !== false) {
        const tokenLabels = LOCAL_UI_TOKEN_PATTERNS.filter(({ pattern }) => pattern.test(serialized)).map(({ label }) => label);
        if (tokenLabels.length > 0) {
            throw new Error(`${unique(tokenLabels).join(", ")} leaked through ${surface}.`);
        }
    }
    const secretLabels = SECRET_PATTERNS.filter(({ pattern }) => pattern.test(serialized)).map(({ label }) => label);
    if (secretLabels.length > 0) {
        throw new Error(`${unique(secretLabels).join(", ")} leaked through ${surface}.`);
    }
    for (const item of forbidden) {
        if (!item.value)
            continue;
        if (serialized.includes(item.value)) {
            throw new Error(`${item.label} leaked through ${surface}.`);
        }
    }
}
export function sanitizeRuntimeTimelineSmokeError(error, forbidden, pathHints) {
    let message = error instanceof Error ? error.message : "Runtime timeline smoke failed.";
    message = message.replace(/patchTrailLocalHandoff=[^\s"']+/g, "patchTrailLocalHandoff=[redacted]");
    for (const { pattern, label } of LOCAL_UI_TOKEN_PATTERNS) {
        if (label === "local UI handoff fragment")
            continue;
        message = message.replace(pattern, `[redacted:${label}]`);
    }
    for (const item of forbidden) {
        if (!item.value)
            continue;
        message = message.split(item.value).join(`[redacted:${item.label}]`);
    }
    for (const hint of [...pathHints, PACKAGE_ROOT, process.env.HOME]) {
        if (!hint)
            continue;
        message = message.split(hint).join("[redacted:path]");
    }
    message = message.replace(/(?:[A-Za-z]:)?\/(?:private\/)?(?:tmp|var\/folders|Users)\/[^\s"']+/g, "[redacted:path]");
    return message.slice(0, 2_000);
}
export function assertRuntimeTimelinePage(body, expectedRunId = RUN_ID) {
    const payload = readRecord(body, "runtime timeline payload");
    assertAllowedKeys(payload, ["runId", "events", "nextCursor"], "timeline payload");
    assertEqual(payload.runId, expectedRunId, "timeline runId");
    assertEqual(payload.nextCursor, null, "timeline nextCursor");
    const events = readArray(payload.events, "timeline events");
    if (events.length !== EXPECTED_RUNTIME_TIMELINE_EVENTS.length) {
        throw new Error(`Expected ${EXPECTED_RUNTIME_TIMELINE_EVENTS.length} runtime timeline events.`);
    }
    const sanitizedEvents = events.map((event, index) => {
        const record = readRecord(event, `timeline event ${index}`);
        assertAllowedKeys(record, [
            "id",
            "source",
            "type",
            "at",
            "title",
            "description",
            "tone",
            "metadata",
        ], `timeline event ${index}`);
        const metadata = readRecord(record.metadata, `timeline event ${index} metadata`);
        assertAllowedKeys(metadata, ["category", "status", "risk"], `timeline event ${index} metadata`);
        return record;
    });
    assertDeepEqual(sanitizedEvents, [...EXPECTED_RUNTIME_TIMELINE_EVENTS], "sanitized runtime timeline events");
    return {
        eventCount: sanitizedEvents.length,
        eventTypes: sanitizedEvents.map((event) => readString(event.type, "event.type")),
        eventIds: sanitizedEvents.map((event) => readString(event.id, "event.id")),
    };
}
export function assertTimelinePagesEquivalent(initialBody, laterBody, label) {
    const initial = readRecord(initialBody, "initial timeline payload");
    const later = readRecord(laterBody, `${label} timeline payload`);
    assertDeepEqual(later.runId, initial.runId, `${label} runId`);
    assertDeepEqual(later.events, initial.events, `${label} events`);
    assertDeepEqual(later.nextCursor, initial.nextCursor, `${label} nextCursor`);
}
export function buildRuntimeTimelineProofPacket(input) {
    return {
        status: "passed",
        workspaceId: WORKSPACE_ID,
        runId: RUN_ID,
        route: "GET /runs/:runId/events",
        eventCount: input.read.eventCount,
        refreshEventCount: input.refresh.eventCount,
        restartEventCount: input.restart.eventCount,
        eventTypes: input.read.eventTypes,
        eventIds: input.read.eventIds,
        localUiScopes: [...(input.localUiScopes ?? REQUIRED_LOCAL_UI_SCOPES)],
        proofedLocalUiRoutes: [...PROOFED_LOCAL_UI_ROUTES],
        persistenceLabels: [...PERSISTENCE_LABELS],
        unavailableRawFields: [...UNAVAILABLE_RAW_FIELDS],
        localUiAuthorityVerified: true,
        refreshReadVerified: true,
        restartPersistenceVerified: true,
        missingScopeRejected: input.missingScopeRejected,
        unauthenticatedRejected: input.unauthenticatedRejected,
        noLeakCheckedSurfaces: input.noLeakCheckedSurfaces,
    };
}
async function executeRuntimeTimelineSmoke(options, env) {
    let tempRoot;
    let bridge;
    const forbidden = [];
    const noLeakCheckedSurfaces = [];
    const checkNoLeaks = async (surface, value, leakOptions = {}) => {
        await assertNoTimelineSmokeLeaks(surface, value, forbidden, leakOptions);
        noLeakCheckedSurfaces.push(surface);
    };
    try {
        tempRoot = await mkdtemp(path.join(os.tmpdir(), "patchtrail-runtime-timeline-smoke-"));
        const workspaceRoot = path.join(tempRoot, "workspace");
        const appDataDir = path.join(tempRoot, "app-data");
        const localDbPath = path.join(tempRoot, "patchtrail.sqlite");
        const hostileLocalPath = path.join(workspaceRoot, "secrets", "provider-api-key.txt");
        const bridgeToken = `pt_timeline_smoke_${randomBytes(24).toString("base64url")}`;
        const opencodeServerPassword = randomBytes(32).toString("base64url");
        forbidden.push({ label: "bridge token", value: bridgeToken }, { label: "OpenCode server password", value: opencodeServerPassword }, { label: "smoke temp root", value: tempRoot }, { label: "smoke workspace root", value: workspaceRoot }, { label: "smoke app data dir", value: appDataDir }, { label: "smoke local database path", value: localDbPath });
        const seeded = seedRuntimeTimelineSmokeData({
            localDbPath,
            workspaceRoot,
            hostileLocalPath,
        });
        forbidden.push(...seeded.forbidden);
        const smokeEnv = createRuntimeTimelineSmokeEnvironment({
            env,
            bridgeToken,
            opencodeServerPassword,
            appDataDir,
            localDbPath,
        });
        bridge = await startBridge(smokeEnv, options.timeoutMs, forbidden);
        const initialProof = await runTimelineHttpProof({
            bridge,
            bridgeToken,
            forbidden,
            checkNoLeaks,
        });
        await stopBridge(bridge, "initial bridge", forbidden, checkNoLeaks);
        bridge = undefined;
        bridge = await startBridge(smokeEnv, options.timeoutMs, forbidden);
        const restartProof = await runRestartTimelineRead({
            bridge,
            bridgeToken,
            initialBody: initialProof.initialBody,
            checkNoLeaks,
            forbidden,
        });
        await stopBridge(bridge, "restart bridge", forbidden, checkNoLeaks);
        bridge = undefined;
        const summary = buildRuntimeTimelineProofPacket({
            read: initialProof.initialRead,
            refresh: initialProof.refreshRead,
            restart: restartProof.restartRead,
            noLeakCheckedSurfaces: [...noLeakCheckedSurfaces, "proof output"],
            unauthenticatedRejected: initialProof.unauthenticatedRejected,
            missingScopeRejected: initialProof.missingScopeRejected,
            localUiScopes: initialProof.localUiScopes,
        });
        await assertNoTimelineSmokeLeaks("proof output", summary, forbidden);
        return summary;
    }
    catch (error) {
        throw new Error(sanitizeRuntimeTimelineSmokeError(error, forbidden, [tempRoot]));
    }
    finally {
        if (bridge) {
            await stopBridge(bridge, "failed bridge", forbidden, async () => {
                // Failure cleanup must not mask the original error. Leak checking already
                // runs on successful shutdown paths and startBridge checks failed starts.
            }).catch(() => undefined);
        }
        if (tempRoot && !options.keepTemp) {
            await rm(tempRoot, { recursive: true, force: true });
        }
    }
}
async function runTimelineHttpProof(input) {
    const request = (method, requestPath, body, options = {}) => requestJson(input.bridge.baseUrl, method, requestPath, body, options);
    const runEventsPath = `/runs/${encodeURIComponent(RUN_ID)}/events?limit=10`;
    const unauthenticated = await request("GET", runEventsPath);
    assertStatus(unauthenticated, 401, "unauthenticated GET /runs/:runId/events");
    await input.checkNoLeaks("unauthenticated GET /runs/:runId/events", unauthenticated.body);
    const readSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...REQUIRED_LOCAL_UI_SCOPES],
        deviceId: LOCAL_UI_DEVICE_ID,
        userSessionId: LOCAL_UI_USER_SESSION_ID,
    });
    const localUiAuth = {
        bearerToken: readSession.token,
        origin: readSession.origin,
    };
    const initial = await request("GET", runEventsPath, undefined, localUiAuth);
    assertStatus(initial, 200, "GET /runs/:runId/events initial");
    await input.checkNoLeaks("GET /runs/:runId/events initial", initial.body);
    const initialRead = assertRuntimeTimelinePage(initial.body);
    const refresh = await request("GET", runEventsPath, undefined, localUiAuth);
    assertStatus(refresh, 200, "GET /runs/:runId/events refresh");
    await input.checkNoLeaks("GET /runs/:runId/events refresh", refresh.body);
    assertTimelinePagesEquivalent(initial.body, refresh.body, "refresh");
    const refreshRead = assertRuntimeTimelinePage(refresh.body);
    const missingScopeSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...MISSING_SCOPE_LOCAL_UI_SCOPES],
        deviceId: `${LOCAL_UI_DEVICE_ID}-missing-scope`,
        userSessionId: `${LOCAL_UI_USER_SESSION_ID}-missing-scope`,
    });
    const missingScope = await request("GET", runEventsPath, undefined, {
        bearerToken: missingScopeSession.token,
        origin: missingScopeSession.origin,
    });
    assertStatus(missingScope, 403, "GET /runs/:runId/events missing bridge:read");
    await input.checkNoLeaks("GET /runs/:runId/events missing bridge:read", missingScope.body);
    return {
        initialBody: initial.body,
        initialRead,
        refreshRead,
        localUiScopes: readSession.scopes,
        unauthenticatedRejected: true,
        missingScopeRejected: true,
    };
}
async function runRestartTimelineRead(input) {
    const request = (method, requestPath, body, options = {}) => requestJson(input.bridge.baseUrl, method, requestPath, body, options);
    const readSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...REQUIRED_LOCAL_UI_SCOPES],
        deviceId: `${LOCAL_UI_DEVICE_ID}-restart`,
        userSessionId: `${LOCAL_UI_USER_SESSION_ID}-restart`,
    });
    const restart = await request("GET", `/runs/${encodeURIComponent(RUN_ID)}/events?limit=10`, undefined, { bearerToken: readSession.token, origin: readSession.origin });
    assertStatus(restart, 200, "GET /runs/:runId/events restart");
    await input.checkNoLeaks("GET /runs/:runId/events restart", restart.body);
    assertTimelinePagesEquivalent(input.initialBody, restart.body, "restart");
    return { restartRead: assertRuntimeTimelinePage(restart.body) };
}
async function mintScopedLocalUiSession(input) {
    const capability = await input.request("POST", "/local-ui/capabilities", {
        origin: LOCAL_UI_ORIGIN,
        workspaceId: WORKSPACE_ID,
        deviceId: input.deviceId,
        userSessionId: input.userSessionId,
        scopes: input.scopes,
        ttlSeconds: 60,
        sessionTtlSeconds: 300,
    }, { bridgeToken: input.bridgeToken });
    assertStatus(capability, 201, "POST /local-ui/capabilities");
    await input.checkNoLeaks("POST /local-ui/capabilities", capability.body, {
        scanLocalUiTokenPatterns: false,
    });
    const capabilityToken = readNestedString(capability.body, ["token"]);
    input.forbidden.push({
        label: "local UI capability token",
        value: capabilityToken,
    });
    const exchange = await input.request("POST", "/local-ui/session", {
        capabilityToken,
        workspaceId: WORKSPACE_ID,
        deviceId: input.deviceId,
        userSessionId: input.userSessionId,
    }, { origin: LOCAL_UI_ORIGIN });
    assertStatus(exchange, 201, "POST /local-ui/session");
    await input.checkNoLeaks("POST /local-ui/session", exchange.body, {
        scanLocalUiTokenPatterns: false,
    });
    const sessionToken = readNestedString(exchange.body, ["sessionToken"]);
    input.forbidden.push({
        label: "local UI session token",
        value: sessionToken,
    });
    const session = readRecord(readNested(exchange.body, ["session"]), "local UI session");
    assertEqual(session.workspaceId, WORKSPACE_ID, "local UI session workspaceId");
    assertDeepEqual(session.scopes, input.scopes, "local UI session scopes");
    if (input.scopes.includes("bridge:read")) {
        const current = await input.request("GET", "/local-ui/session", undefined, {
            bearerToken: sessionToken,
            origin: LOCAL_UI_ORIGIN,
        });
        assertStatus(current, 200, "GET /local-ui/session");
        await input.checkNoLeaks("GET /local-ui/session", current.body);
        assertDeepEqual(readNested(current.body, ["session", "scopes"]), input.scopes, "current local UI session scopes");
    }
    return { token: sessionToken, origin: LOCAL_UI_ORIGIN, scopes: input.scopes };
}
async function requestJson(baseUrl, method, requestPath, body, options = {}) {
    const headers = { accept: "application/json" };
    if (options.bridgeToken)
        headers["x-patchtrail-bridge-token"] = options.bridgeToken;
    if (options.bearerToken)
        headers.authorization = `Bearer ${options.bearerToken}`;
    if (options.origin)
        headers.origin = options.origin;
    let requestBody;
    if (body !== undefined) {
        headers["content-type"] = "application/json";
        requestBody = JSON.stringify(body);
    }
    const response = await fetch(new URL(requestPath, baseUrl), {
        method,
        headers,
        ...(requestBody ? { body: requestBody } : {}),
        signal: AbortSignal.timeout(10_000),
    });
    const text = await response.text();
    let parsed = text;
    if (text) {
        try {
            parsed = JSON.parse(text);
        }
        catch {
            parsed = text;
        }
    }
    return { status: response.status, body: parsed, text };
}
async function stopBridge(bridge, label, forbidden, checkNoLeaks) {
    await terminateChild(bridge.child);
    await checkNoLeaks(`${label} stdout`, bridge.stdout.text());
    await checkNoLeaks(`${label} stderr`, bridge.stderr.text());
    await assertNoTimelineSmokeLeaks(`${label} stdout`, bridge.stdout.text(), forbidden);
    await assertNoTimelineSmokeLeaks(`${label} stderr`, bridge.stderr.text(), forbidden);
}
async function terminateChild(child) {
    if (child.exitCode !== null || child.signalCode !== null)
        return;
    child.kill("SIGTERM");
    const deadline = Date.now() + 5_000;
    while (Date.now() < deadline) {
        if (child.exitCode !== null || child.signalCode !== null)
            return;
        await delay(50);
    }
    if (child.exitCode === null && child.signalCode === null) {
        child.kill("SIGKILL");
    }
}
function hostileFixtureForbiddenValues(localPath) {
    return [
        { label: "runtime session id", value: RUNTIME_SESSION_ID },
        { label: "provider request id", value: HOSTILE_PROVIDER_REQUEST_ID },
        { label: "provider permission id", value: HOSTILE_PROVIDER_PERMISSION_ID },
        { label: "runtime message id", value: HOSTILE_MESSAGE_ID },
        { label: "runtime tool-call id", value: HOSTILE_TOOL_CALL_ID },
        { label: "raw command", value: HOSTILE_RAW_COMMAND },
        { label: "matcher key", value: HOSTILE_MATCHER_KEY },
        { label: "local path", value: localPath },
        { label: "prompt text", value: HOSTILE_PROMPT_TEXT },
        { label: "provider id", value: HOSTILE_PROVIDER_ID },
        { label: "model id", value: HOSTILE_MODEL_ID },
        { label: "fake secret", value: HOSTILE_FAKE_SECRET },
        { label: "raw runtime payload", value: HOSTILE_RAW_PAYLOAD },
    ];
}
function assertStatus(result, expected, label) {
    if (result.status !== expected) {
        throw new Error(`${label} returned HTTP ${result.status}; expected ${expected}.`);
    }
}
function assertAllowedKeys(record, allowedKeys, label) {
    const allowed = new Set(allowedKeys);
    const extra = Object.keys(record).filter((key) => !allowed.has(key));
    if (extra.length > 0) {
        throw new Error(`${label} included unavailable raw fields: ${extra.join(", ")}.`);
    }
}
function assertEqual(actual, expected, label) {
    if (actual !== expected) {
        throw new Error(`Expected ${label} to equal ${String(expected)}.`);
    }
}
function assertDeepEqual(actual, expected, label) {
    if (JSON.stringify(actual) !== JSON.stringify(expected)) {
        throw new Error(`Expected ${label} to match the deterministic fixture.`);
    }
}
function readNested(value, pathSegments) {
    let current = value;
    for (const segment of pathSegments) {
        if (!isRecord(current))
            return undefined;
        current = current[segment];
    }
    return current;
}
function readNestedString(value, pathSegments) {
    return readString(readNested(value, pathSegments), pathSegments.join("."));
}
function readString(value, label) {
    if (typeof value === "string" && value.length > 0)
        return value;
    throw new Error(`Expected ${label} to be a non-empty string.`);
}
function readArray(value, label) {
    if (Array.isArray(value))
        return value;
    throw new Error(`Expected ${label} to be an array.`);
}
function readRecord(value, label) {
    if (isRecord(value))
        return value;
    throw new Error(`Expected ${label} to be an object.`);
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function serializeForLeakCheck(value) {
    if (typeof value === "string")
        return value;
    return JSON.stringify(value) ?? "";
}
function unique(values) {
    return [...new Set(values)];
}
function parseTimeout(value) {
    if (!/^\d+$/.test(value)) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    const parsed = Number(value);
    if (!Number.isInteger(parsed) || parsed < 1_000) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    return parsed;
}
function requireArgValue(argv, index, flag) {
    const value = argv[index];
    if (!value)
        throw new Error(`Expected value after ${flag}.`);
    return value;
}
function helpText() {
    return `Usage: pnpm smoke:timeline -- [options]\n\nStarts the compiled loopback bridge with an isolated SQLite fixture, verifies the scoped local UI runtime timeline route, repeats the read for refresh proof, restarts the bridge against the same database for persistence proof, scans HTTP bodies/logs/proof output for hostile fixture leaks, and exits without starting OpenCode or calling an external model.\n\nOptions:\n  --timeout-ms <ms>   Bridge readiness timeout. Default: ${DEFAULT_TIMEOUT_MS}.\n  --keep-temp         Keep the isolated temp SQLite/app-data directory; the path is intentionally not printed.\n  --json              Print a machine-readable non-secret proof packet.\n  --help              Show this help.\n`;
}
